From ff220f6f3b0aa40169416af5678213a24ecbbfde Mon Sep 17 00:00:00 2001 From: AI Agent Date: Thu, 19 Mar 2026 18:04:32 -0600 Subject: [PATCH] Implement robust logging with flexi_logger and update CI to verify logs --- .gitea/workflows/smoke-tests.yml | 27 +++++ .gitignore | 2 + Cargo.lock | 174 +++++++++---------------------- Cargo.toml | 2 +- mudserver.db.test | Bin 0 -> 4096 bytes mudserver.db.test-shm | Bin 0 -> 32768 bytes mudserver.db.test-wal | Bin 0 -> 1030032 bytes src/admin.rs | 8 ++ src/combat.rs | 8 +- src/commands.rs | 2 + src/main.rs | 50 ++++++++- src/ssh.rs | 9 ++ 12 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 mudserver.db.test create mode 100644 mudserver.db.test-shm create mode 100644 mudserver.db.test-wal diff --git a/.gitea/workflows/smoke-tests.yml b/.gitea/workflows/smoke-tests.yml index e048653..6276890 100644 --- a/.gitea/workflows/smoke-tests.yml +++ b/.gitea/workflows/smoke-tests.yml @@ -170,3 +170,30 @@ jobs: grep -q '"shop"' rpc_resp.json rm rpc_resp.json ./target/debug/mudtool -d "$TEST_DB" players delete rpctest + + - name: Verify logging + run: | + if [ ! -d "logs" ]; then + echo "Error: logs directory not found" + exit 1 + fi + if ! ls logs/mudserver_*.log >/dev/null 2>&1; then + echo "Error: no mudserver log files found" + exit 1 + fi + if ! ls logs/combat_*.log >/dev/null 2>&1; then + echo "Error: no combat log files found" + exit 1 + fi + + MS_LOG=$(ls -t logs/mudserver_*.log | head -n 1) + CB_LOG=$(ls -t logs/combat_*.log | head -n 1) + + echo "Checking mudserver log: $MS_LOG" + grep -q "World '.*': .* rooms" "$MS_LOG" + grep -q "MUD server listening on" "$MS_LOG" + grep -q "New character created: smoketest" "$MS_LOG" + grep -q "Admin action: registration setting updated: '.*'" "$MS_LOG" + + echo "Checking combat log: $CB_LOG" + grep -q "Combat: Player 'smoketest' (ID .*) killed NPC 'Shadowy Thief'" "$CB_LOG" diff --git a/.gitignore b/.gitignore index c536345..b83bd81 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.db *.db-shm *.db-wal +/logs +/manual_logs diff --git a/Cargo.lock b/Cargo.lock index d7e0910..dd9c3a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,56 +61,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -335,12 +285,6 @@ dependencies = [ "inout", ] -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "compact_str" version = "0.9.0" @@ -385,6 +329,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.28.1" @@ -694,29 +662,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -809,6 +754,21 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flexi_logger" +version = "0.29.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a5a6882b2e137c4f2664562995865084eb5a00611fba30c582ef10354c4ad8" +dependencies = [ + "chrono", + "crossbeam-channel", + "crossbeam-queue", + "log", + "nu-ansi-term", + "regex", + "thiserror 2.0.18", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1166,12 +1126,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.14.0" @@ -1187,30 +1141,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jiff" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "js-sys" version = "0.3.91" @@ -1387,7 +1317,7 @@ name = "mudserver" version = "0.2.0" dependencies = [ "crossterm 0.28.1", - "env_logger", + "flexi_logger", "log", "rand", "ratatui", @@ -1423,6 +1353,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1512,12 +1451,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -1815,15 +1748,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 457f726..5b45c07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ rusqlite = { version = "0.35", features = ["bundled"] } ratatui = "0.30" crossterm = "0.28" log = "0.4" -env_logger = "0.11" +flexi_logger = { version = "0.29", features = ["async"] } regex = "1" rand = "0.8" diff --git a/mudserver.db.test b/mudserver.db.test new file mode 100644 index 0000000000000000000000000000000000000000..4e86411b5803e34b1e4767ce981907694f15c1ed GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYBBVU-7{~E%2$%#=0*Mkpn$(DqfCvbPvLPmH>Qc!H!^u(yRi#ANxaxULcGhaM9q(jM#m?hoTkIG+ht0O#(>$K~IO}$A zx6d|bHK+5QU6akWza4M0ZFjPJ@zh6ayK_$DcK3O)=gem7W6zMyw%z@#InSQ@WOd%m z5QPLJAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(T zKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NWe|N$J{gFXMV%}pj*2hC6Mz4 za#4t29_A4W6G9P6QjQ8#B$UUg#FKDeUF|Cpphm{>Hi4NFjmj zCE(nxrI3Jk3OF}jDJ0;X0{#@`QHoQ71~jA*5%i)reRz{;#50{4e8tzSX9KAm<2Wb0 zlS#YIkpcmkcb(?Qt!YydxQBpqU#>y|-YJmZJGZS}OTY*`L?ERp!xL1cIyGs+3%pDR zUZ)fN7{CbLW&)G=fY~f!2}@bdw)DPQ|jSYjrxkK5cMya~s=5?{w;P+WK$-H>XZJrW-!o|H+{#p`}gI#-73_ zFPkQClKlVgb5Q%^`~Ux@UOMawY0?`9O410)x~ z*N>=bTk#(!+){n!?9-zO&9^9SELIb~D)DNUOZuwMCysTAFSqrY9XIRs1)WdC_vHKS z^t`8sF6oG?4lPwal3c~lO2yAA8+$s4Uqb)^1Q0*~0R#|00D)WuRwd-&qbe%o?;M{{ z7wTG_t}fFIbH_K=NpohLFn5M;?g=NJKEt=;7kytW^6heqsf#t=+!<%j^?iNLsWVTQ zbB^z{8Rs0{?Jz?PX~AgNK62I>bA7XFPCvc-VIf^L%?yq+;_<~99Hwgxn(jMw*4!DV z%$Vcr{8-=g87H4mbNXE0l0ZN4_d4)4Q6zpnnmr z?Ime{8V{Z18aC>L3R#NA!rD@Ei7tMs532Qvc=CC0$Dj1zlpPPOpX&>E{2RvlIX_h| z(2hQG^$&jj#bu?|FWRL9>k&5u5I_I{1Q0*~0R#|0009IL*fRyJ8iBhrM{wsUSC?tLB4WoG7TTA3k5|U zJDR8uYi8P$g0y6O+HduckGEsA!LFEmX9~T0;M#htz9aGts$Sncz1;oXc8cTszPQsU z4)@ugb7*=h_}GGBqh?OnT`D+jmcc1$PxoORL3e%D5v10Mu#O-_VXXvV9f7?!KxbaS zch~kinqD}vopl8KhZq3_5I_I{1Q0*~0R#|00D-+i!1}xYeLCv`fp6aR=$2RC{T}NG z_KI`QBoROW0R#|0009ILKmY**5J+1`@Q<$y|KTTx{!;H_9l`LO*sT2ta$ZL;%=1uc z9f9YeqH}h7UmhZW00IagfB*sr^d)fl*nNhNnmAFud|5JNZ)%3v>^f*_B4{6rn7ce3 zmKL<%pBQ z;<(|%M;&^o{EaDgm_4B;>dm0GXpt65m}c6u!t^M6+VAy{WZ!AKc01ADnN;t-(Rgh% z7Vhg9LtCsyVjdGU_=Z`BKHtLK+cN?=$BN8e5wC5hy5p*>tpdSJK2z;|@PCq^MM{`Q|KbrI- zNagP7N06dm9YOMcGzzWz&b+|KZ_oV7L8Bf$gmnZ%;(x*zBmxK^fB*srAbpP`uO3tUiZ1uOlcL_CdFG1j9ZU{@G6N&qD+dKmY**@+fdck!Sd*2@~Wi z&rIgucZAj>^-(=+radW2OTVZ6R{z59$ z6hYPzu#Vtt(YxUcyMIG!J;k{B3&wR{Qo%Zc)W5b_N09vAtYkExGcOSMedwIkzyCoM z>j?7rzo5J|0tg_000IagfB*srAb;+G z00IagfB*srAb>nh5>v;6}* zFVG<=9ea=>Q$YX$1Q0*~0R#|0009ILKmdUfx3_C)f#9(60;#${^!EF|_2C(Beb>$l zG)YPmUjEv3(lVGc=d%$&009ILKmY**5I_I{1Q0-=vo0_vSs$2`ZG9jS zZ;Vx2cR_PWy=wGc7bs|LJbUaZx1Vp<1@4!W`|(Er0R#|0009ILKmY**5I_Kd!4ycV z3(QOw2P$(b4yX;95$iQCaQXaejGE!)MdSqr^NjP^2q1s}0tg_000IagfB*srbP1%@ z1^oHV3z)Ch+;Q?Fb&Kq}K&zy*c8!Ba2q1s}0tg_000IagfB*srAn^GMr0N2*lUad@ zIb{XHs!H?qtdFB%lKmY**5I_I{1Q0*~0R#}}Nnm$% z0gu(RFoSM|$qfc}Ul$l=edd~NflX`6SB>AY?w`k8x8M%9T^HCaDVuv55x<520tg_0 z00IagfB*srAb~SYVW^2{JQj@CX|YABZfar8a+z>zj0R#|0009ILKmY**5I|t)3+(Tn?CDk<5Yw{q0;#&dlwjg- zR~&V8%+3pJk(4b%f8xvl0R#|0009ILKmY**5I_Kdy;@-Ofg&(4vRh3cVboiBfwa27 z1nKxwzdWVkQ`Qme)mIFYM*sl?5I_I{1Q0*~0R#}(lLUHSN03$*Fwc2*@hd01eIa>) zJ?R2qE(joi00IagfB*srAb$MY)4A{9i1t};6u6gvX4M6x5fXh0EeCE0 zAbjE8vIB`B20R#|0 z009ILKmY**5I_I{1O{55#O?K%i{rIgLNgQM&66d7&b+|-Raf8hS>%atr}6@A1O4&5 z9|8yJwTcC@(k_XA zxFLW50tg_000IagfB*srAb`No6v!kmkk*f2+_wK(_sqmQ?y&O$&63hQG$+oe5kLR| z1Q0*~0R#|0009IL*i!^VKZ1GkNN=Tos;5vEx3aS2x57d>wSAzWhP3Lip+@4dkgl5M zj!gt~wJ~n(D4@j>hN=e_8qsh>GtE+E{(|Kz0s+6@iUp+gBXEtL)VkrR&9~TjfeuNb zAHkk-88Ht85I_I{1Q0*~0R#|0009InfnEC%Sb2fezJteqddXq8K6&pac3z-KQkpxg?n3iK$AgtjEv3(lVGc=d%$&009ILKmY**5I_I{1Q0-=vo0_vSs$2`ZG9jSZ;Vx2 zcR_PWy=wGc7dX58)#qMY_D-u^7ig7~R{Rk_009ILKmY**5I_I{1Q0-AFa=U|f!WEz zz{H#i17X#u4XV1XE$cNeaMzcbe*EAapG>1JFqmhY&qe?N1Q0*~0R#|0009ILK%h%t zfa?N!Z|evy|HUKHWiw}f$~uBBe;y%#00IagfB*srAbj7!aw!o&f<*Ua3aOIZk*YCIBEvqizQr1gKo4DhK00IagfB*sr zAbmT zHL{}8KXupr0eAfTZm1!xI&7$scr2u=rn%#<_;s~0ZvB0~7E2ha9$aWd!;$pf0d_xv ziJk#oN8lDSOXUUbTe`mS(&MI_VCMz4O3GHHBa4}HC;|u|fB*srAbFV3&c><@&a#+xPIQWvZfPDl@>`BQ30ifynypgCnG`t0R#|0009IL zKmY**5OB0Wp7H`>&T}jM`OOQ+cOSk)ij3V}Bw_-xLSDep*BOID009ILKmY**5I_I{ z1Q0+VGl8A+0^${=sjUdbleB&W*BsvbKlN3oO)OQ~L|s7iBUn#Ag3O3H3IPNVKmY** z5I_I{1Q0*~fzMl@uYLq#Jma$KN1#o=ZksFI_CS$nLm<#S_Ab;VxQ<%2DfO^%!gG-;2YOH<)(g=zD1+qCC9E$(~2q1s} z0tg_000IagfPf`1g!dQt$Af3Q|LDBcmn9b$SakvV5pYc-fB*srAbI;3kxR6J~q>*4B<786J4!G%UN9MMd(RGGhE`HDcm@3&$B zZt+#TW?Nv>+VWN7ua!TzepAcXPwl)whop2U9nM5%R0tq|00IagfB*srAb$O59#gs-PfVZ{-D2b%Ey=UVYfZt@j;f=LMQ1rHQ;ihQ=I*00IagfB*srAb1|RUGf5Hb%D@h>Y>rv2R^pz0xgo# zLS2B%8UX|lKmY**5I_I{1Q0*~fvg1bR2P_&tPf1ewmy)EH^!>1yP&zGUNw5J3p}uL zlJx6y$4HggJ#5f z%?tc-^|X2IZ|xr=FTiDt00IagfB*srAb+VWN7<^R0$qVKXmb!Ub+1zsk}g&vfgpCVL%8VfB*srAb& z+GD;}|A%lYFVL>ELyZ6e2q1s}0tg_000IagfB*uH6!40=KxYu3hoAagz6|ODX?cNT z*K2ov`JeawpS|y3o1~BzaO8<+PzWG^00IagfB*srAb8fe&*k(Xi8{^iV16nL$sCsar5e-K)(=1iyFIc`J5b*o0SU_4of@j+fd2qXT zb#gO8S+pXcAAuvUFb0JH0tg_000IagfB*srAb>zN0=@Mku<`<_eFsNRzU|^gH=n(~ zofl}5lqT{5*+6qF0tg_000IagfB*srAb)Dg6;pj`}wV-V|HDjMN(R*3vfjvfB*srAbH=Avdk#hb0R#|0009IL zKmY**5I`Vbb%EK*!ob9w3Ik!)s12&Rt}W{|FYxB(f|ov1re8!|fXf&G1Q0*~0R#|0 z009ILKmdUp1)N zA%Fk^2q1s}0tg_000IagfPe!9#<&V9sys$rNbDwfZtza_am6-8Q^sUZZW-7USQ0z&%aqQ@3f!Vd4a8xvQ_EGcKRHT z00IagfB*srAb_Z`aZSG0L>OK;0!DJpRe=_WeSs(jv*C zMWIqdpF&5SNd|)e0tg_000IagfB*srAmDs~Jmm$%#OGG}^P3kKHu)>1+Qg%bA`ugi z74ibkzt|Wd0tg_000IagfB*srAbe1Q0*~0R#|0009ILKwyXpWW2w?OliSwKl$}@>r#0E_7@nUr<`FUfB*sr zAbKF3#9cUFn@B;8yC($?_4V{Ad8*^N*nzMoOt3H69NbzfB*srAb^k{Y%Y%1Y6;Cu7-~qX4jXDD9t-KJX;z1H&4`MJjd49(9nxar z2tByah=wDYX_hMU7c5^92>AV0EWj6|3#94-j}0rociPIY9A)PP znk1!(yg-J=9EJb_2q1s}0tg_000IagfPgJ9z4IJI0%>)DUp;@(<~dE@4cT>p7D;KLF2H4t00IagfB*srAb#C%iz2tFvk(mng4qgL8=fz6V#x!)SR0Rjjh zfB*srAbmg;!1s@r_77CmKAy@8v@7jUBY*$`2q1s}0tg_000Iag zfPfzbBcm{<40tg_000Iag zfB*srAb>zV@&bQT+P+v_I`w9&SRjk2fI?n?3k?AT5I_I{1Q0*~0R#|000E~83`AZa ztslV)??2UX*>}EDXXgc)C8b%Zaq8q57Xk<%fB*srAb)a%%fPLk(%wVMC3?VqoHi{A>R{!F^-1ofqh!AAut;Fb0JH0tg_000IagfB*srAb>zt z0=@Mku<`<_eFxVZeA2!1LNkZkd4VQLX(BI>l{5zuN*`MpV(9aF@J5T3z5!bM(l_^jQn+x zv{Dzy>fCcM0tg_000IagfB*srAb z+PbewU4Y9N0R#|0009ILKmY**5I_Kd90iPe7AQ-b6=g08qg<*s!BtM!1iW?Nv>+VWN7FS_sf z@Ve0@hE*4EDeEPrP26!q009ILKmY**5I_I{1Q0*~0S5|Z}Xg`Qh+!KMh51v5EyQWrL(_P}&_hafXBd z0tg_000IagfB*srAbx`5S>Ae9#=d~L&DwBr_i$<7PZ(2szt z3;_fXKmY**5I_I{1Q0*~fouf^cpbrlz^+5I_I{1Q0*~0R#|mzCfPx0%GEGEB*P+ z3taQ#hAI2|A9$xo!~|r8ynypBHb#g50tg_000IagfB*srAb>!h0z2mg#KEPhtq8@F zw0;Ecx{9CQc1p!lrAnKq3y6LM>*+_(r!EH|fB*srAbDuN!2f2em1$dz^V(-k05ngd4K={2q1s}0tg_000IagfWQzH$asH&)_32Se^cXq z-%jNP*k53Xo^pnb00IagfB*srAb}J0-rn*o^bN^7iXsO0vp6e1Kbcm z009ILKmY**5I_I{1Q0;Li2?($zd%|)g5Uezc)ID-MORyS0a^4UP}=B6;KUQpm=Hh! z0R#|0009ILKmY**5Rjz4@&Z1&z?)q^f^A=4^|Pw!ueMml0$D@_ly>q0Txkd(fB*sr zAbF`76J$$fB*sr zAb)vbi{3t0gouVW=UkI&7$scr2u= zrdb`*H6tn>HpcaEbx4beBlO@xBN~oqrdg`YU$A^dAmH~~u>iODDqgcKuxV}is`2px z|B#MCnNcBt00IagfB*srAbO&MNx%W2-+o(#{JsNlFuWfeei~3;_fXKmY**5I_I{1Q0*~0b5{z^8)84!vfQC z3=4!+T?;lubzO~U!H6o-1-s-0(&_?F{p~l0KCk2aiB7{;(*$q8L?jT0u48v^{{#J-F4&z zxQr1%009ILKmY**5I_I{1Q5tkAfI&se}3}<2Yl)AGv=;)@||D8Voq6suxiu>RbAJX^_myBWx++?4_UBY*$`2q1s}0tg_000Ic~Ti|o53wW%ig&A}!Ol~l+`?|m|G2c`_f^Reww_n|M zLycV**eofV`>nwnAb0901Q0*~0R#|0009ILKmY+}2xO5LxPI$1 zAOESmZ2@@!XPi1lfdB#sAbnFdzgFKmY**5I_I{1Q0*~0R#{b0%KeS z6;+-cKk!HDqk6bHq-#brWCoMZd~$)es;dal!x7?5imk8Cy1@T-yt8!ko9Ew<$_unB z?NB3t00IagfB*srAbetyZx>7Eu9(yZ{#(0tg_0 z00IagfB*srAbY%`#% zjd5$w0WFp=R6V%Rh=wDYX_hMU7c5^92>AV0EFi5P!RF&vJ^1uv#`o>KKnMK@9C?8; zCxg?n3iK$Agtfx7c>G0AbK^;|8mlhgQr1gKo4DhK00IagfB*srAb@Qo3UM+3H02jFl~Q<)rnb`Jskb<8&*F8X@tc7 z0vVnN4nqI|1Q0*~0R#|0009ILKp-VBg!dQt!oCOn;?~0tpPgJ>VATcaN02(LJU{>e z1Q0*~0R#|0009ILKwyXpWW2w?K^HuFTk|m&Je?7=0R#|0009ILKmY** z5I|th7Z~{c1>XGSzwi9}%kjCXyub#r(Ev9D5I_I{1Q0*~0R#|0009ILaH7CK>@Sek zk6_NMzn=J;CyzYO$_vP%CxOyNKLRJ7c*cYP0tg_000IagfB*srAb@}*^_3U!$pzl* z`VkyEqbB&b6R+ED6$@k$6;RsA3vi_&fB*srAbBv!paDH4dFS!$JT71Q0*~0R#|0009ILK){&-1K*Eeo;=c9>7VK; zl*O&AEcvalP&OCGYqf-CCJZ&CRfi2V5|4#+)ikR^x@JVh!^XHCt`2E2afBXRXhg#i z%`{7u`3sh>2n76oD;D4uU&U*-1vafMUp4-o#T!Tb&#zzp(9R2VNJ@v&;Y?&kg#ZEw zAbeK zH0Ce_5I_I{1Q0*~0R#|0009JSfdS47oSO^_Ov^DW5LR_9*bvoqHKGM0sz?{?k{3v; z3!E|f=SO{`WZ5LUF3=(=Ez||LtPwx}0R#|0009ILKmY**5Xed(Pj!Jg$@;*gZ0iGw zcw?;Ex(k|1>Q$rny1=(zuQ_t*>en~gb%FaO<$me{S)F?hMgRc>5I_I{1Q0*~0R#|0 zAfI)CnaScnWp2d*wLvptz2*f<|LbQRPmGv2io5`qF#-r6fB*srAbH@Qq zS%HZ;Wd*{jQ5#fsU0c>`Uf{-0)_%J0sJ6SP3+$~IFY`tK0R#|0009ILKmY**5a_qS z=TsN)SWOEv=vJ8AU|{!kfnj34seS}!)@(m%+xOo2sa+S?EGe7&t-%{0fB*srAb2q zEt0ZjFbB?OBY*$`2q1s}0tg_000IagfIz4_njW4LXrfvA|_ARsO0@4VHegu{SHv|ws009IL zKmY**5I_I{1hNqrTKf*V#ne*!4*u$`@O3wxxo%vMHD9YP(4ll>Gj)zd009ILKmY** z5I_I{1Q0*~0mlgxyDMa)E|k#Bgsa5u^_Yv}wVL%zj5JvS=*$aL-S)Gy->SUq!c<Z5wNI;3kxG-L*o&wO%$ zx2mfM(8CepO^U6r&bmPTi`Hl_gD6-z>z1OK_P$u0tg_000IagfB*sr zAdru|z@iI&bl}@nM;~bw3uF-$P{<2#p&@_(0tg_000IagfB*srAmCJifyfJ_^&`0a ztf}Rp3#QGn^8(G1(yY`tb@Gf00R#|0009ILKmY**5I_I{M+yvlKZ1GkNN=Tos;5vE zx3aS2x57d>wSAzWhP3Lip+@4dkgl5Mj%@~XwJ~n(IiST7hN=e_8qsh>GtE+E{(|Kz z0s((=8-cWb1alXC@YP2j_|E%wUZ8`11dhDG7!(2sAbx*E}f5mhuN+$ArNRu{PTYuZz5uRribyDrcoDJ|3mxS$b0009ILKmY** z5I_I{1Q5tdAWwCHImyKYld`Q3B;t*+YU?g&E~!_I-s=LvFa3Dt<7dwE+jW6fNol1n zkkz^8U<43A009ILKmY**5I_I{1oBlEn4K&POw6e;5LS)apsMTIvR?B7FH9Tv&9nX- zyoR~}moWkeAbrZjLcJ%GfJ~w7A#*82>AU)c0YoNo&jD*;1<(M>XD{z&#y!S0rKrvO-?K`4<}_L;wK<5I_I{1Q0*~0R#|0pihCF^8(`F($rRj z;z?RRg5Rw;;PfxAY`U*hX%lq;(T`v~{RsNh9<>qk(yWcu?5u6uA;k!V98(~n?K&MF^@00IagfB*srAbuIP;H>TKx#55fb|gWOyPt3;_fXKmY**5I_I{1Q0*~ft0`y z-e2I|_rLMI`bgE0$;Aa$U4VWBsnf~>1Q0*~0R#|0009ILKmY**hNwWs`wLum@Hs#E z+BaW3E0q^ue}N%-${98S2q1s}0tg_000IagfWV$FF!1{ee66CY`IhDTc~g0T4Pv7K zZU`WN00IagfB*srAbmw9${ii6@>h zA%Fk^2q1s}0tg_000IagAW41Y1$=UWH@kiWXWdx6x_H#2m{ly0MN~j(CojO2h5!Nx zAbp)rRcfB*srAbQ$rny1*ZW77PUEqF6 zxu3c~R_C6B5kLR|1Q0*~0R#|0009IL$Y)((X0kX?nOku{ZP1KZuX%xg9ewA+6DmG) zkr&`HMgRc>5I_I{1Q0*~0R#|0AV-0G)&>0e%?lj**XfsD>)!OST^DGTlveTrIbH?4 z3;_fXKmY**5I_I{1Q0-AZxzT_U0`-HD=;yqtUy>bYJ;k-Ys-4g3lyC7yAK*F?l_LR zz}|ZCGH(PBKmY**5I_I{1Q0*~fqn~oPIUo~)wD2!ZiUGW26kT;7$)YM>PPV2+H>MJ z4nJgrT^HCaDVzJP!5bie00IagfB*srAby#` z%RfB$lRpHGCokZPQ^zO}KmY**5I_I{1Q0*~0R#}}w?OZC0hjguYqkY8tu0?QzVX?y z7sZyA{@tnzNFyZr5m*k~5I_I{1Q0*~0R#|0009IL$VOmj?K|ieQ%mhT`0_=6-fztf zTP77*^R?;%9ZE+wQ|DL&5I_I{1Q0*~0R#|0009ILaGXG~yFxbVLJ7@GxJuk!kGVKr zt69&)NRuUi&b&bVL3i)JZu*PQr1Ao7%6iAmh5;df00IagfB*srAbK6 zX@?pC1Q0*~0R#|0009ILKmY**94X)xb%D+xKo39lyL=hc1=8{Yt=mc~)uNGa+4~N* zNeX!ZN1k{Fg#ZEwAb8b+5qlVa1ycJC-uCPNy;y6L{$%F`nk1!(yg*jc9E<=0 z2q1s}0tg_000IagfWW}#1w>e2T8?3Xu&QgphN!Nq5iJ-|MRUSk@&ajffrk#M@@#zQ zN|#+1Xpxi_>H=KQ2q1s}0tg_000IagfB*srWF?TNy1<;|VuDH8)&~;t##ps=7c`gD zt48m2ft#m1uyMuvC5PK}fmTUrr7n=wx#wU65I_I{1Q0*~0R#|0009K@RTr3@EDTJ{ zsW1>$joP59>)Ntj^8(i%ed)jERDXItbpbA81Q0*~0R#|0009ILKmY**aujfOT|n<` z9l^goS@i9%Pri99>j-i@_q+@N1Q0*~0R#|0009ILKmdV!)dhCM1+1O~Su`a`t|RD* z3smk}7qD6nNNctQHmxmRHU8FT*DO8bkt>E z>5tZVp1d4U@G5pb0ufB*srAbV9f4a+FO?TK@%o$IeE!5W|FH7{TP0T&=A z2q1s}0tg_000IagfB*u6BG6Yq0x_s@+4Up%!qKZwxb28%w-t#t1Ty^y2IZ{sp$H&= z00IagfB*srAb~5VcPxzSKj`>f1fe!sQs;e1kwnJ{RJ{S z5gdj90tg_000IagfB*srAb>zhU CommandResult { if target.is_empty() { return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin promote "))); } + log::info!("Admin action: promote player '{}'", target); let st = state.lock().await; if st.db.set_admin(target, true) { // Also update in-memory if online @@ -124,6 +125,7 @@ async fn admin_demote(target: &str, state: &SharedState) -> CommandResult { if target.is_empty() { return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin demote "))); } + log::info!("Admin action: demote player '{}'", target); let st = state.lock().await; if st.db.set_admin(target, false) { simple(&format!( @@ -142,6 +144,7 @@ async fn admin_kick(target: &str, player_id: usize, state: &SharedState) -> Comm if target.is_empty() { return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin kick "))); } + log::info!("Admin action: kick player '{}'", target); let mut st = state.lock().await; let low = target.to_lowercase(); @@ -237,6 +240,7 @@ async fn admin_teleport(room_id: &str, player_id: usize, state: &SharedState) -> ansi::error_msg("Usage: admin teleport ") )); } + log::info!("Admin action: teleport player ID {} to '{}'", player_id, room_id); let mut st = state.lock().await; if st.world.get_room(room_id).is_none() { let rooms: Vec<&String> = st.world.rooms.keys().collect(); @@ -329,6 +333,7 @@ async fn admin_teleport(room_id: &str, player_id: usize, state: &SharedState) -> } async fn admin_registration(args: &str, state: &SharedState) -> CommandResult { + log::info!("Admin action: registration setting updated: '{}'", args); let st = state.lock().await; match args.to_lowercase().as_str() { "on" | "true" | "open" => { @@ -365,6 +370,7 @@ async fn admin_announce(msg: &str, player_id: usize, state: &SharedState) -> Com ansi::error_msg("Usage: admin announce ") )); } + log::info!("Admin action: announcement by player ID {}: '{}'", player_id, msg); let st = state.lock().await; let announcement = CryptoVec::from( format!( @@ -405,6 +411,7 @@ async fn admin_announce(msg: &str, player_id: usize, state: &SharedState) -> Com } async fn admin_heal(args: &str, player_id: usize, state: &SharedState) -> CommandResult { + log::info!("Admin action: heal player '{}' (empty means self)", args); let mut st = state.lock().await; if args.is_empty() { @@ -564,6 +571,7 @@ async fn admin_info(target: &str, state: &SharedState) -> CommandResult { } async fn admin_setattitude(args: &str, state: &SharedState) -> CommandResult { + log::info!("Admin action: setattitude '{}'", args); let parts: Vec<&str> = args.splitn(3, ' ').collect(); if parts.len() < 3 { return simple(&format!( diff --git a/src/combat.rs b/src/combat.rs index 1307ba4..a485f99 100644 --- a/src/combat.rs +++ b/src/combat.rs @@ -69,6 +69,9 @@ pub fn resolve_combat_tick( )); if new_npc_hp <= 0 { + let player_name = state.players.get(&player_id).map(|c| c.player.name.clone()).unwrap_or_else(|| "Unknown".into()); + log::info!(target: "{combat}", + "Combat: Player '{}' (ID {}) killed NPC '{}' ({})", player_name, player_id, npc_template.name, npc_id); if let Some(inst) = state.npc_instances.get_mut(&npc_id) { inst.alive = false; inst.hp = 0; @@ -351,7 +354,10 @@ pub fn player_death_respawn(player_id: usize, state: &mut GameState) -> String { .players .get(&player_id) .map(|c| c.player.name.clone()) - .unwrap_or_default(); + .unwrap_or_else(|| "Unknown".into()); + + log::info!(target: "{combat}", + "Combat: Player '{}' (ID {}) died and respawned at {}", player_name, player_id, spawn_room); if let Some(conn) = state.players.get_mut(&player_id) { conn.player.stats.hp = conn.player.stats.max_hp; diff --git a/src/commands.rs b/src/commands.rs index e5c3b7c..a71c188 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1222,6 +1222,8 @@ async fn cmd_attack(pid: usize, target: &str, state: &SharedState) -> CommandRes }); } + log::info!(target: "{combat}", "Combat: Player '{}' (ID {}) engaged NPC '{}' ({}) in combat", pname, pid, npc_name, npc_id); + CommandResult { output: format!( "{}\r\n{}\r\n{}", diff --git a/src/main.rs b/src/main.rs index 41cf891..e9dc139 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; +use flexi_logger::writers::FileLogWriter; +use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode}; use russh::keys::ssh_key::rand_core::OsRng; use russh::server::Server as _; use tokio::net::TcpListener; @@ -18,12 +20,12 @@ const DEFAULT_DB_PATH: &str = "./mudserver.db"; #[tokio::main] async fn main() { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - let mut port = DEFAULT_PORT; let mut jsonrpc_port = 2223; let mut world_dir = PathBuf::from(DEFAULT_WORLD_DIR); let mut db_path = PathBuf::from(DEFAULT_DB_PATH); + let mut log_dir = "logs".to_string(); + let mut log_level = "info".to_string(); let args: Vec = std::env::args().collect(); let mut i = 1; @@ -51,12 +53,22 @@ async fn main() { i += 1; db_path = PathBuf::from(args.get(i).expect("--db requires a path")); } + "--log-dir" => { + i += 1; + log_dir = args.get(i).expect("--log-dir requires a path").to_string(); + } + "--log-level" => { + i += 1; + log_level = args.get(i).expect("--log-level requires a level").to_string(); + } "--help" => { eprintln!("Usage: mudserver [OPTIONS]"); eprintln!(" --port, -p SSH listen port (default: {DEFAULT_PORT})"); eprintln!(" --rpc-port JSON-RPC listen port (default: 2223)"); eprintln!(" --world, -w World directory (default: {DEFAULT_WORLD_DIR})"); eprintln!(" --db, -d Database path (default: {DEFAULT_DB_PATH})"); + eprintln!(" --log-dir Directory for log files (default: logs)"); + eprintln!(" --log-level Logging level (default: info)"); std::process::exit(0); } other => { @@ -67,6 +79,40 @@ async fn main() { i += 1; } + // Ensure log directory exists + std::fs::create_dir_all(&log_dir).unwrap_or_else(|e| { + eprintln!("Failed to create log directory: {e}"); + std::process::exit(1); + }); + + // Initialize logger + let combat_writer = FileLogWriter::builder(FileSpec::default().directory(&log_dir).basename("combat")) + .rotate( + Criterion::Size(10_000_000), // 10 MB + Naming::Numbers, + Cleanup::KeepLogFiles(7), + ) + .write_mode(WriteMode::Direct) + .try_build() + .unwrap(); + + Logger::try_with_str(&log_level) + .unwrap() + .log_to_file(FileSpec::default().directory(&log_dir).basename("mudserver")) + .duplicate_to_stderr(Duplicate::All) + .rotate( + Criterion::Size(10_000_000), // 10 MB + Naming::Numbers, + Cleanup::KeepLogFiles(7), + ) + .write_mode(WriteMode::BufferAndFlush) + .add_writer("combat", Box::new(combat_writer)) + .start() + .unwrap_or_else(|e| { + eprintln!("Failed to initialize logger: {e}"); + std::process::exit(1); + }); + log::info!("Loading world from: {}", world_dir.display()); let loaded_world = world::World::load(&world_dir).unwrap_or_else(|e| { eprintln!("Failed to load world: {e}"); diff --git a/src/ssh.rs b/src/ssh.rs index 658ce4e..2751cde 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -83,6 +83,8 @@ impl MudHandler { state.load_existing_player(self.id, saved, Some(channel), Some(handle)); drop(state); + log::info!("Player '{}' (id={}) logged in", self.username, self.id); + let msg = format!( "{}\r\n", ansi::system_msg("Welcome back! Your character has been restored.") @@ -171,6 +173,13 @@ impl MudHandler { .map(|c| c.name.clone()) .unwrap_or_default(); + log::info!( + "New character created: {} (Race: {}, Class: {})", + self.username, + race_name, + class_name + ); + state.create_new_player( self.id, self.username.clone(),