ヿyBUJ" UH#l٨(}V@*@]4ve=( R@"(@ v>@|M TJ#6 ::i`w+2&t@}P܀BiGct-Mnꛃ;:q{\ HU7 p'ռh5Ͻ4 -J:}Dt;pɇʎ/%>iQ c]5p->y8Iq>CCmvtT lU[lGD|>a(} @ER7xzhy{{3ˆC']vt`{΁W 7I{>0@q]Pu|3`>_|Zӣ;1(ݠ @{:M8ښus4}i;h}m] T;)@(ƚ`r>ֆּA@q 1@ := }>y:CA`Tt ؠ+Avwsn}|eMݟg̶w@}Oy6%}뾵vp47{v[9|>85xϻuTsipmփN/9G]hav>}i˾^ٟw}NN} >{ڥ=n:;}>3ǽٞ }}_go޾wh|trz9@_zCIS/wl^[xA;wWv8}upyX٫U!˛}}M8C{{=Vyӧݾ<3w u= ۣo}v3G56'@[iqt6v{@3ssg2;v0t;oCJGbFd Y r}x=Oy|Fϧ^[mw$yF'vFFRYށ=@i2hP: 1n>> =Gq#t:7 AѦ#Ʉdi42hH&&FL&M'Sy4520#FF   &@ &CS)M5D>GlB$&dD4"d;%v$|ЍFLF>|N#``@tV;Dh1>dGӨ=?AGӠ$ɔ\h6(`jm(>&D;&%H؄}Z0}Z]Z#+(^jtP(|򅐉Po1ƤF4Txt@4dFHI#ɑ $kDș% d5̢dV4"yO He#DpGو@@~Rv*ԈR1S*RpEOvXTR:T=T$|TO12'&? -H#?‰$Zb51>CR>CDB#*tmB Pp/(|e 7P!d!'"~' bTJA>k"~('V?/ފS U,L3D3|FFqHA h0C$۪LP}eHa 5J2H2JjCdY =顥F?A 0jCF~GT'I`&\TCAR?] $T d6~FBlHaT`]"j*G$kčGk$St~0))FE3"ȑTHԑO7?TFGO5ȩEH25,Ȑ2" a?l0GTI?b*F0T#V`S\k#40G*ɳFѨ`#\> &H= ASSfСNr m8H!A&M@:A c)GhMO{OiQ1^p!~W/|_UI"~JE94>O Fi")І B`PB `.#* 8  .88#..*8"``⃊ 8!(b 8!#(. $  #(. 8#.. #8૊(. #..8*GD4EG\A @pWpLq1@pWpL10\LqpL?j#`b(#8b#j: &" 8&(` 8.&8 8&.&" 8&".&8 8&.& 8 8&` 8&`jE!]M tA GL0GB胠⡂#h胠⡂#hh# b .&8`b`9[F?#bcvjh-Gm,V0꭪ͬidۛbmJ5/͌anVj5 )0k Ԅ7I&MtϤ޼ċVQ2 5vmk"+888b!88&"88#8 "(`8#"`8ੀ"`8`)a0qTpp0pTp~ੀ*& `8ੀ*&`8ੀ"&`8ੀ"&"888D4@tpEqCp0pSqCxT״.VP()GL qL qL T "`8& `8& 8)a:h 8`88+8)qĉM!fhLF`888"`"8)8"b`88=@:>`@hUk&1$AB &PPCHlHuU$ @u5`6 3"f0g$ff8`88`8#8〮(bVh"h+&`9thh"hCBf10Q0 AtMqD0qGpA0L00LqTC SpLD0L00LqQC  Tp1Cp0LpGpjDБ4#D` c ` "bcc `  ``)&*&8#c ` 8"bc8)&8 p#j&8"bc8)&8 . c&*&`#&*8DSDE]pG01\pC1qS Uppysgoo$Љkɯ9G+щ\Yar#mٖj*k&Y[ ql2t:I!2a9 lAV܌m4]u !!&nH@&&o}|; 6.L@1W\ \qq  R U,F,K@-Vб [@@-Vб@ R U,F,-Vб, U5 [l[BlB-lF@dA2!2BlB-lm  [l@[BlB-ll@[lB-ll[Q  obhh!-6 Xb6!bڃbض+b-#b؂؅kb #b؍XXb6!b؃`ضb-6Xb6!b؃bض#b-6 X-j6!bؠX`؅kh h؍X؁`؅k` hکb-b6b- `d)2y]/m?朇36"s{&T^( @A)ITIX,Rĉ H$̱$)Q%UiIJE" t)":XD. DЇKP A*7~a&갟֘ZR<c3q`bUt)TeR<_hS]kZUL6 1B(H({8$$$"$-HwDŽ*Dnh#"wP 0c fjI AR B?Y|#󦈇(z5k#FC˺:;6 e$)(MqDP 撚bQk Ld6Y$.fD>|ӳ6C$tjB,'5H0b2đ#Y)W28DqFBlgiCJQ1"Cr*5ي(e]&,QBE Pybt7RXTH*)$4gX)K%$B ')L(vcrCmjڸ6 $`I,P"GeJJ(&Ѷ4I70)3ĒFI'(R$leq`a &HD5-TM*@"G${BGHܑ I\D8Ш2P^ $m" :bf4kI$hJRHYF (Kh5Un"BpbID'`diaD`Bu!CpچCXCT${DI$6;'2j0Ͼ2َHv܌t$2MmLd 2E4 e40 ɼ0oSf;$/Tz\^h@RA HYtJ$R ZJҪ 54)# 5K"L8t$)]eҺ[(EJS  %(R`BPH A&(!Ja*RJTRFD8`N@A0$)Jc%(1D B a "D Ra2L&D ٕY*JW[,TR @Jc)BRL1I0P:;%^I-Ք)JU)RTQI°0SH 0!D "UR`ɐHI"E,ДD ,l DF XHbQRScHo u#42>'^ 攊s&wM7j) 77j*G؈ґNcJ51Q5DXb>ȍEi#_5֑֚h`c &k#kt0Xb#Lװ`FE<Ѩb#X5D o B"oDGO&j|4#j,Ge"O*i,GI@bt5MпB&YIQ&e cI;4aIMdhʘ6 Hކ8 j𠀛-#B SPlyL؆!dkCD`=`kª`z#F G#\aY0E>#zCQIHdj)F) DSJG#QHi"E= z5F)) %#A{H4Oj %.Ѥ ]X'CV>Z @im c)yrHk6&5z`@O4iz E zAטR>\|Z 0R>Gk kOj)q!R>Ć) R774 ҄O >0jCzCP ұ @o   ]PܮI( &vFRlG7oPDt!H1MQOb}0i $}3R? ?|?jGCSB~Q`GCJ 4r?  4h m7tMC@MHSR0S!Cٿ" P7kt㫢]7礯I![bV+b+?CkCx !"!B<@Cxt}? ! f?4h gQxm7tH[ h+4k??&j )&Zϰl,a6 k?C}0a !Hk'wEDf7=;լO}pԟ>ߟ?yL,||2s?X{o-ϒO2L$(%rj==d>ԏ\z>O4I< sPqǐOGz2Xc~v#n<9NMF-{ʻ$- @HgGrIg\?zˏF2C yvnM͔ bv Oj41}ucc9wqG4ld`=>d >xx ; w`>/<8-_Z/φ׋Wx yvz='ȍ1z.ב.tF:#*Witq]ҳ(Y;z&AwHA~/HWx {`H\i"xH[ !hX`m:B t PxY^>2t A @}}>~(?2`?@`2 dI2ϒ~o n+ȭ+1_lޯEWz+4Wo ]ڮWzo UZ`W;?[+JW=Uޮ諽\WoE_[Vޮ]]71@ y3 ]аy w0Gp/H = h`?m?Gx: r @6w bw-/ łq#3;h!exP$I]VGhM a+?UmU_= 'N};μB>w'< y22M S륜i\z);UVܷ|-$uLSl36͉e (@6Z/}Z2c%z"+VW2i3)yH{%~pcfkGK?(tsK:CPO:t#!FIN},f*WGИV1@dstŴq[j554), bF`Fd4_O7ibZ4)7Nww- -/ / .zBwb} l[ʋ^x/|/D_ދFٽ;;w)mS;ޥOT)SvM;Hxw :@R.l6hG@tiGxt6H;z@wx;ywțа} ;ж!ȅ!CCz!H>Cz^Ew^Eɵ7P޺,Mj}P;w'D `?z)`)򷃽K}Ҕ?f7oSE&m6' oBޡ !x?Lv7x'DMo%bo~h'&NA,NO&>0'b|a?LO #HK][]uxd]CyktSz-!QlME M z6D$޻Ho(E6zM |AwmI_޶wȶ̻z]k]]kȻ6[xC0~V .tOI?%Q?>!Gbb*RRiG瑇?_hPP4qB~mau~ܜ~=x?\93j;6gI-syQGϊ!DQiK^ACr?2hj 33 Gn ?$솦[_4:1ww43׵n.۠޾T]kzzWoXWzo̥N<؞5-:IMoKNy'"ZoO@7觯zZzޛ襧I7͉-7<@#w;ދI輏;Gwy<#?='{wxmU>A:?#D(kq5!4$Vu)VAS=6/i ކH(QIT*/.F C FG==CB)h ,B+-DEK(J+iK &B*`hKFQr? >(1X P`1X QCÁF2ža3g$"^pB?TR?"muFҕ1AaJ15bb*\jEň҅f,8G\KXT(%*6#YJaF+v 5pPf>W<`G!Gݟrlw#3eSKRsg2)?r } 8 ɸMh00&Aa=I G9Ժow:'# ~2|xGPdž#N{hO6ǔd &I>q|r9ԡ {MZC n_b$LD3) (_)δl|VoS"N6>܈}Otl)32uM BNԫ:du@zTMq1$0{"p(K!qSN9AI'&94 {")rc CߦdE~aDdG$=2XHD ᎣhjAl" " `6C7<[6]t&Nu hhi0c1~p@?y@mhU@ pgyCFSYd.a0=@N)$}oUoJejep(FsI>$O$ڦ哬<;\GOۣL'Gԛ?7p䳽LFo"Hb;9&ؒfGo6G5yW\?[—i]SOU姠B}>&jM2Rzb'$6i }2łe&)!_*zM>ӋpVT¦q k$Bwaw;zX:QGVQ_'9cgiII"H5tFBiXh=jJiDДRbVp`rX*p}R"jԏ^?҇6xؿ99G8Мx;0fEuAi#BRr Tԟ(kj94d|aWx69wog?cO=CcmlƘcCm0i6Ɔc6Ch#-TyAPI4V-Ki(z{"uRCL)LmFtli5*ʖE520!pr)Y:6NkwgC);Ae,cB80c|*,;diL,Re4ɚDIM1Q͓&d2mAW]_MaOUS&g; ImZ듳[۾e5y_J4 $ ;#;coJh*j9:f)Jb>A)áI`bD) b) A0c :c "J)C!aIIcˊ`I( "DN'N!JtBpc~}`icЃi114hl7K.ebNZ2_45x^+sV[@~7\WYܴͭ'.v׊/qq@4EMyffnεDzє#LTCda!ȈG2f B,Myo5(1XD`L&PD)fDMaMfHJYhl@BQFIB3)$B`I2d 2&PYbPD&ԒlZ($) %,DS ke1cM, QgQbs`Nj֪v}WuQ b Q"6L,K-U[-Jv>ߣ-O}?IZ`LIBO0ʐi`J~lRhxHxX) 2z$'{OIQMd)#DϸR`&T"`9%JlWnRҀ-iim-lf[DFPIm/muxS/q!$#1Fb#)A2ɤR6XM2J) DDFQJIlT2+SC*jZ%6)ՌBfcY#MKF&lB-Ai-]u(KXAdDt0pdK(J?"ێANܛ鹝p: 8G y@V8:OTBu|OLL {PۧLkQR:Qc'JqZRؙ%eEmW);NRM4&?#J`?yE -4^b'?p}u3ɣjg6B]佝b ]q,Iũ-U h% -1JZ-_X^v\gV3M4elWH5 0_z*:G& {K`.Y0ٳ> {D i~Hux57㧺ErZl6ahKW5J]u‘FJ+΂֒O"X yK.S4Di7ijIn7~ْQlL}qTZ G9=W}Yʑ#Q:6eO<8_FQ2 C<{s~sDk,8?<H+^Ã?$×硬S愭\L͝tg/ kH a S[0qM;A䖩²u=eOsU:~^X:2f'; vx!tv{'ϣ:oj|o,ig˿uxDOGz@wC?<5Vu=;`pɇywau]Qi|uwtl70>6狳;i:%i-eFZA?;0u?doip^K:+aҫg7SvA:*uK:XKj¹sC7}: Ñ1MKX,!x3*'R]?n? @)P3Ϳɏu$KtT[5UVWnmo]ZF/w9_K"4nqd#oi_п{7?pڷW*xnxT0iU) s)s0KJExMR\ܭ ۽(6yƮR~9ßBc$duXri1e NGui™tJ?<~q"Mۿ?gb?~;.Z}OӕI<蔦'y蘩kZg_6I" 7b‹3(]Ypp@@daa'@M2J,̘,U. )GN|;JImZ_=qJs#)4hd[[K@놰ڑǜ*rs9#!ɏ>8$2(zX}YUJ~:FL*fJSmYBİ14,ecN40iLhici M6Ӱ 8v-;%I3&R$̩)M ?JY8jb Ux3h<\z!7t!B3 !\N@NPwO,>?MrەuU)JdгaΖ IU[X4O}0`4ꨘ6,bbho7Jz-O۹3nX+tQVIc;ؑ#P;pvg>MJid4#RLRbkom\'#8Cblz~ J%)DRU)dZYkIjʨR)Jj17*(N8t$#&5ՕK8BbHj׌׆?"*TU-)UIID&4q54hZZjڽhzOz`lj6ۓDDŽ9F49N\0Rd$k~O\lmPl~?3X\TGy"eVgV= &׮v6|F9G@do&fS = ASJx0XEPY'OX?y0`n:QJS􉄢9PoN*\Y`$<~qa]Ig _: P?Da"jclR^ɯ6eq4,zPa裌 tlFdK ꍍbcCJfٶmfejUfmmdbFfYdF<ԶDDJ J ;Q|DPѪʿNlK\p~y^u{YmΧ%rŕpiwK:,rr.u^. Nj;:&pM&.8EܺV+8Sc#G )כm)H”ILOxAHIϲS& DGK"*$d$QIbŒQNL03IG{!bG,"HM($*"DYd E  6HL $ĔI)JR#ӖH*C\RfJH*4ME_IL# }2Dk'PGDQ Sn&y-)Ԕ4< EJT=w\%> QS UMJ%9`RWl !ϼ)l"'$ ҧ$Ì0WVHs#獤@p'ei<Ϣq3I V 6 )-ʫWc,ֿ!?[)AñI8n SS%FQۦ`K3`4`1',߉E[Ta0\T7GA"Lyz—3TJ4ji inTSCQqf eѝ9Aɜ {DM 8RE)9DG.4:8#J4RN:sc%&T=e, BЄ`M=Ճ+ B˜!HEbm)0bS&$HH$С4FЦi0Fm4`h$Ig"``F:HZ1bR3XA. h6Ҷ64-1m iLG 'C!)`1P2$%`h2bFR 1A[ɳR[7UYe: Wi0:F!4d@c4e!,c+WAlc@j xK4XIl&`_ I;덶<P?oq'ҎtkCzmDļgF >rGN}aI]8G0FFFqAb#ŔaO?2Q'Șԙ%~a87Ambc;ۋp ̦>9$>N*QBykE#wCʏ'/AKЂ:b#|)ClvτCL0;XxC,O5ҧN mksW1'v7È*gs3̊Qs-e~0?ef;iފ{ԥmއK ?Q{٢"M6*ŵHj ҶTͷ(Gc@| Ap :!jR0RA$HhT6KIV+5̖fTɓ&jLSjح-[5-BLjM Ref#JfDY$iZ+5,ٌCmjY53Y j-nojZ ClhTxOLB:v DU064!`mw6-n $``h Wײ{Tʼni`ԫiFŊUa0&mEJY4EJ"6FfmefnJM!7ʫbm(6 %ImΫ5ye͵r"+j 6]6I6/ķVk(Ѿhf-YYs<(#~Gjx[1NSpbm'ym5 mbk̑*GPuq霜^QE%!Jz uF(cf6 TlԖɭDQ--JifcCZ+652j,J+6Y!61CɅ3 7Xk ER[RʘN#MK/;$"KyuUeipU1Lk ")1)].#7+֝YJky9CG99INj8Rrs #T7SƆ,OZv6/?s($vٔȟIG|?<$0SNXqݧh9qF3#m(D9I9eb^ie ƙ)QED NVnuǑ놙4pݶ)IFf U.\11 gaq'<@a4U{φxgJndΜ$ Kyrs$ɍ6Rf)UBx``?BC~>r)ŜسhsJA=̘VmiY˝~6G\Tsg\Uu\eZ_*孰[y1o2,)dlZQա4J %5*- 5DZ VТ]vu,"qy@Sk #9)mR&%dF+ESDԜpYcf*}EUL:C`4NBBHP DT]{t^u瓒2H2Yi3Jd,@,۫ʕ-_8=?i?zc)Gbf4ʲnL)f؛bj&56<&h#rF&]2=XuOB+=@ijm'E.J_}gseC@`)ŁC-*#h–"a,aD)fedŔiVhjB`eYnHk*)pUXX* JRQRQlR0T&nTo-ח-K,BԵERQkiJ^i7LIt\HP9眝]y.L2"8K"-,DV3sPD4a͈eiaZh2RC%[`Ȏk GcGGL Ɂf I=5cBk9'8L8YI D"pDbyRfNMFCwsQs CrP thN+9qf&C+aΕ̹l}|]0Q!CFœ J?h4XpX@j"s{OY!rՆ1\Y™) *?lsnhqk y$t"{Cŕ:NiG>!22ԚN2pɨښ vY+ܓbUy24sGsny:W%$˶&0{Q(4Fh7FsiM 3I&#$53U%G 9"9AfyƟvt`CAA[aY (BY4I!C%0²DffvCh+`uC\2F Ee0oɬQ3- )J-}aH_TJ>?'x<:`~Qi0Oaķe]_ʽKVowzOU<6bI'lRjeʢBj"3SVc~Kr7*gᓸ;ū0NNNm);@AA_y PjRNedf06"`>0öN&kI;L~E<>,m=<9aHR X% K$ j#SIв#IDS%@ "GܲtY<;lO*T7?NMRQ2Ș"0O'bx"`5RTRJI4'^TYtC#!cU0{Gro;rɹ9͡.0d a6fO9 $DG~[UmU!ONC)M9#pxazizg$'xO=)dQ02Cl7S0Ǟ;4&)JJQ00)G*&C y󺚌lm\VG4~#hIZT .H~iq4 vF͍Ct'rM$)2IeKDo}$ȒsCǯ;E; q}4 S$Սɢ#* 2B hed2FdbfpG(Fra SaNHNxݜRa -cLbڣA_{Gb!|SUYeWQ6ɸOMk5q>hvGv?쯏~hL>5cpem0:7: |ĮD<|d2~:=ṰZkZ#sv<݌H492=rfSs#܉ȌO*9O4CZZlkFIφ#u%q 9J;&imk:ݰUVtGJ< >R>mUUVB?>xm( @GE!Ԏh:AӼPqLg``A(G<_|pfaQ$u8󳠌$F8PZD%?(a=U98>k&jr5l022?x>GZYx;pqh4u`K1%~E,:<\<vZ!;;$:P[bxj~3kMRQ{H| iiqN !# z$$'xCș >Q9@6ܢLGZ{ I߸ jBvЊz˷沑Zk΢Vy9Sé^lZTcMh-0v||R_О>vlΧ֣C~|X{~Zf6Oo??orEw;PF+&?Q׬U~εM4gIA\a𜁗r&"w&$%Tb* rE=p{9D/gm.Î]m̻ˬh$|0KVZ?nZ,E%3%P4@X[fpzbE* ̃/*UlTS4X!9FҐ~~qQb]"=CG꿣]Hj{|#^gYZE ˺oaaѹ}fgqrr(5Kf>4ڦQa7CQϸ>n@b/Dmx~)IZX$4uԑ5T)96!$ؾٗ S⺿IToVύo%ir,rFj5̓-$ӗbSXWr&c]Y^Md¬Z0YT:@·{S- xKyg*`/pt÷#.Ӥ{B,.|2>7\m*wzw~NiDon:>M&&!SU}܁Rޔ )8~W=^s___HyjS1y~}^~z竓\.q֦:xˁۄ:YϽs|X^1u}'XrS<゙Nw ן-g+8_,nţx4#Nwq˾Sx2i w;çF5RKĴ쮔38w|D>QS6co{m"\q,ljeXC:,hY4Mƒdö$GF֛S_GͳzFܖ>IhX?Uv9S">Y|}/mV cQ/Ӧ-m=yng.}#_]Hx=QN/ͣukr{{$߻J#ѳhT-#;a?Yw~T9y6Zc >o-=bϣZDܹs2ԻC]cEty'H^JmhSEIh\.C-D)hGc ?1$%nkOB:3Rs1_9QUs<1kt( .S}~4l(gJfaßkP7?G\qeyko 1,;cADg_}+m[1F] 4oIQeێ8p6ϽvK|&AvpϹ`iqn-ApQD4\&&&6}\ha竅G?EΔpoL6(O, 'dIne2IlO!_;oӢRmM d|eIߟ)~l]D۽cJy@CB\Iܬ_5W¦:\̧/9wU%3̬.yr;q;㾳rυNGHRꏒAXfGL9tm߸5dI :į~R/s`ن/ղ~'/,Yb 8҇Njr9K;um1ӿ}`曆_Jcolz}_"fn:ǽI=rsY߰q}9V鿅7u.s֭U?#}zvVׯ^K}\uv!Rod1+ak^*tp.u6Om/%IG,j^;pwce=]N,bi9q1ouه)R0ɻ}-=co~TZQP{ 1.SIݜ@?=<Ox?~8S$cJ3پ(u#O6cӋr~pd$%_{Bq;O*5+>_ _6vxg›G%W64 [.h1Cm ڝw|rFj}1^>{ԓlNCdʷ ]k v L+v1`յŮmD˸Ѵ ~]~M߉fLpDQÖ=)U.;j^uT<ß(tȏ5moFv/Dꃿ\#rC˚[eOlz;*kM9\fi#_ovNn\tآUûä,^ֵ~#xdX[lsֺ[ΆLl7YR32ZrQgG<q a {/~{k_s`+_W'*!.-"*sh}6PzۿMkֹ̿\bbO\c˼Ӟ)[Fy)Z7ߺڞOi:MEƔ- ge\tl빡'vݪ2ֽ4 Cdpw2lXhGr]l.u̹Ѧ2m7>r\g%qV6,D6,ICuev4{_ lȂE8=r\t H0[=~Wrs:3!ᵻl<|βkڷPҤNL5q(E=W ǎN}'w[ߌw>mo64q}'4MȮ~d}\ՌqӜ ۗ- x 5LJa~+V]sOnuqR۶ =/|8\8AlsR6sx437{?VWm"*}[ƼnFd%%UpcAm.aju-\fZk<;w6'LIuҊn\Aj[>O 㱧*ƲZ-_V{krMpc;:Ml1jjZ7:n?67Up^f M6Xkqybì[kZkal&qm9oϸ|ni @nK1xK=3Κ C4͵Wy}8hvzlk?S`2-_f/8_m]8o7Hn^!pM'==؛',xr^GKӌ2WH K֗LlݎώטթٮQd7uum1bpY%6 dbېI8!36pXNts/FxEjxomgܧ,{I?^nbuip~{+#𣄙=oQh,^L</\<-uOhBquxZ=k]^U3E z2eӆmI"=-=D/D|'k6ۺcN;|xpN;z[u]}stx]-.wp|7|?j7߃z##LөJZ[8Z5?pMͷ!6Qó֏)kD9Y DO8㢃3|%\؞i.- 8i6/jh8S_lm]?Hut0ӧЯg.Xi냺NY|H'|{H';} $ڋ&"+qUs<<`[Ywo~{E@=ÎoZuNE#Z³iY}܎+>h;.3ЈIpvV׺L[eצż6zXħץ?iUs9ƿ:9Fᛘ/⦟Dݕ'Q%.5Ö `ѠpMgphkﶜ#!\Rm,6Ov~pmܽ,܌YБǦ!ptCKswԴBM>~̿lō1l;-{F ٚ]OePmHx{X\5;XxkQUjzV>*sDL{1+Llz^Vi MҶwqt܊hoYkԆ=xVH|4|x/Qk3EyۯFw\Y>g,~si^ U-Քl]k)6ZyvmcK祱i<}}?4˫Cǎ>vnޞo=.c…h]o>ο~eH,i&yF8rߚV=MbK%NJl.;N'W%oʲ">C0STeOdk`ّN?n9v\XDR<2\vɳԺÓfZX4jӀt1ƵOw0磃{DY*+vo8K+,Iydi->sR=We]]Wv}cMY5x:BlcHd#$4:c!AFi Ϸ&c[n# sL5>{.lSq(akCtt_JDyCn410Ci668pOр\|?_Ct:g~\AysZL<1_e²fHq-}7deFUk6m&Llrb }!ʟ,`Y)b3KRy3ze$Rpp8]+I DMaݘP!Sˍ ҝ)՝\)U*T?4&fE)h%JQ<0ɝft /<4Ȉ4 4ЧB:l6͡>;/Q}?[L(JE++ĵn3cᏸ9dx>{"dd@~j8%}3Fv~!-!g(o Io'NCl 6Qk JhF"LEdVZj-_+h 56,[uZ:`@y@ԶijW8kuZ[?^yva$a+띪|KsЄdP s%lqAZQc=HiFHhfhT=T$i6̌ ~5>3MUUMMvw,%ɕ%gwewdûU\{)4Xyا m klRmgo'e[ejg9e-[dv;6SUMU5[k}~I|⿭tYi斖P<;_?b"2"25 PI6Lou1tpgزxq}B,S:a~`ލ*!RjjA)uz}7m#@7C=ߓMflڇMoNqcym*^9*aDː8 ˦#Rt@UEE-.DCY5##ڞ(rǧ@y/K}0Q1Sj}Yļ"Yxd'HN)ǻ0mnAI+NtdžO'fg"#5Aihxsh<(۝s'wG1Odi ZdHTKpriT  b:,0J0`f`qj0Iac<iWc1kK*+P_+o@cUQ%eUXY2G9aNJp?h0ˌ>fnd ZhbkQ3'6G C'rt#Q)fj3Mѵ5%x)Ϊ'^L<+އwf}?]u}5L˓6, X2DJ2aP1q0,1K\Wi2L a0Q5((m4 5( cF(ȅJ[Dhq"@?#ooM=$1;c2"2ͽNK,[~,rLUM~MKKhbMVZn$`t;sGw@He'7Va9գTqjRQjU$Ji0LFbիKUjњq {~I,KC h12!C]u{H>C_dBBBBDe ,y'IF{!C *7QKJ6*#=(^LU7v{$?bKyeb*du]S[tjTnfը1fp䩵-UU3\[-lTU6vh0R(@p?zƗ bզSŜW49Q֙9 `?-i-؟nDE`P:[3r\lvsDjB*Í@Xk[Ս52qRJH6BXggV7Qӵj{^k@qdS|[%!5=9Sh1\jҤzC- SC{m?Piɀ+hسj3|lt=yb72;d'UH~6T s"dZ_]k.4hfIΰi]&94lA+J|Q XcR@ubU FG٧j59٧ &IF v?;nXFn3hn\;,-VTI"[jUx.ɛi&\wnvme$ŖiEK2R, .P]] ,2 !:f먚$b "hN &TmmcfjHחSנOq{Ge7W&JLcREhm6LQ0%%%DRDRS! EHVZYfVJ*xK*2WmfԖ5KT6S*$iV6yjjȒdFDV^]mšr _5-Ky8qeY7b1xyu)m8Js4x)%I"UDL"y4Rcn.^lYMValI%([[[ElV*m׮,ILcmRYlXѓmҲڙ5EQRTQYIصQQh,5,̴o-庺kHU+,`,F@Hhʱ"IZ#"LdʬmiR 0cV ڪjډdYYR,MTjI< aGv1,ݵ*Re4Šc.z";6U*2S)eҒDjK)e(DFa+4rߤ͍Wt.\1əQ%hYD8 c!H Ֆ4e**mmmP(8 H Xƅ@16یq{v$$h1@뮫:Y^H#&@C@E }}4)\=^tN*#:r%Fpr|<[OYX㤛({!8/9u8zso~}Si~|.zc~+6?Lw򝱾j;sەUO3]9us ^Dںd_AjYOq?<϶3RqYYtw}ݻV|1w鵺wɕ;ݞ~~&*ͳ=ho3&_[<5c? H G#,-[na"f7X詳9Ѳm:*9{{WXJSA*44 -Ԣݤ޽KT ϧǓmp0{R~OwAy&gx1Ϛ~軽Iާ`QebN" ?<6t?!PjLJ0R4(&, AHQK3("hF@‚ p0-}C1L K1%2)e0i)YLE%%If YK(LdSXR<@Ӡ cfIE5`YM%0R%,L` Z`S4`PPh  Em mq(Ď&6hbh!Im%8ؖ4SzCێ}=~ۖMŖw#^e,,}g4nZ `~-]IѵX 8:hCNBhUbsxjO`NysϑBm ۥ5cJ۹H' w$kHsĤ%@`䆂zV3DC Չ"i4vsdۼ6X] TUG h9xt3&E0FG⸐GT 1=s vS )HO;HbdZM,$i"*, chIbc8%8Hқm)#_A1^]8%S a2`]a0¨ ,Xi:j~HNyŐ)6'qEr[j P"Ys,spByȵm6r4Jp+Ƶe!cJLg D(\mam7˅  &Goy87ᨂ^'l U@Bp>X*C)2*s$Ye4''>Rg0TdԚҥ.TxnagW/H(M'+Q( ̌=J? )$G&n?r9T`1h< -]Zjߤb⵳T' ^oWrDQ.Ā;_)U.I? 0pL8}U)s FLvq )b4rGXueSM}AxM$?uGӯq<>Jv#vn2&pvCQjF5j^1ɎHC͏ƚYڶ)XIS# 99RdS/X$5VRfC$}'#\tjc+ SȝhxE-U[1cRsV [y͒Ju4C\n, ,Tlfl('e:q~"*Oh|cE R䲇"Z-jY]滌bSƱA`*XFyM"ZRyM]Jiv'&\|uj10@?(d(P!$F;>|GW.I&$x-돇wׯ4xUԚ]s2aYbbl6 jAAIBHE)DHj)JR֖E&E0Z奶VU"S[3JbBGEMbP#Frf}1d`FC#YڎFGj;+ֺ e8yjMc84! Z#Hc6 Q|6} _GtLz9S/uìb?)PD֢{U'Eh_9".~ovy=qϧOVRժʕ &ʘbzc(,aZ ,29S3DG*O4yBо~W=;p?>^= [urZrT$`@nU e@I Tw 7J[Us K~ӄ@[k8~u$F>XeIh N6H2$ EIIȩJTaT`)]v P%DI*PHrxCI! fFt&'4TJe|@45ivװMPP^#MߦYpB>zqGG<saOVf=;397l%~hLIH mYKD!!PloK %Q+]` $?&V.a񀄇~+ 0Y-ťg6fV 0le0x( tN;'`h$9)a}{ H8&YkwIs ;$3;$\a331U2Z&oGNNBQ8plRj@D9N< `Pܲkhu߮ɓ7vM`d7f `]q9k|لsmmRm +"iLnN^dvt]v8L3Cqv 797"qr541 3hZŁaَ@F&↱~_]RceH 1.B^-p&kysuGIbDXim)BZI>)QȋbFa$A(R)msFce;\;xdvGhtZvFIy&GhGhv쇤4'w;72kNG),9A`y%0;mY) ,4BQEd;9'U#CѭԒzqlkw>(U7^~Ix\zo1sjfË]sS38O5OьE7<&nID*\&I.:K4" d$/1UYr3%=Wd C\ʜ_5̈Fim3VUFK0d'4sQa6(ɃCC$$^3Qbʼn2K=Zg3i,Z-|Nq8A$P2A ^ʵx 榟9D6l[=ck.ϋĢb2Ⱦ xjy̗.h3kk3{gͯ9s:S36c9/{ZWuiU*ٌb|[2=- 4m ;KU02PoWoOe(1h/5M,ICsFYk36ߧ>-XHNGk0?ك<)gt?t#r;pGz$h;2?7z= > E@/r +aF$7/ !%OC!ׅdJ'6H58<]{+@& ]9I"|g34\&dZ|f}cAh'9}‘ &ça:cMC`v}1G\|4Hp(pnp.Y=_ժZk[p"{ID#w^79$hBsca2\!ŨZYVZ?(uUU (I T`1V/:>G4Sl`~(x\ֳ ,)dB,)j2N8w |la$M9ʞB!t7FLR6Ӄoȿ^-)"H3;Gl3=j֤ADA`[x/*~YbƴZ l@ Y`a`uh$JR͌Y7uuiTԭ*VHTLV],R?{k%W[ʸIu鼪m0 ,Hdp '݌̢L ff  8i!FgBƯᄲme-OOU-]5K!^KB c*ؾdlcaӳ$QS426Y`|x',C`~#pMs8KdHO%ILI pT[$K_~n!fdΜo: ScɑrR mz}{\ma3ut/ӃVٴw`|60A:IJz xfbƢ F|xcГA6G1"DzQ&OTa>K'}q;гhE_ϠyEHۏ16%nEG`7',DB:_pcYGgY-~M%Jv@3}\ЛuJ?9mZ#'<:;ѺNS]R] .;$i^pG(f0oA2VTLIG?0tG6䎠h:f`m<㶚G9;hn۳ѲLٻ=YQ^GNď:"uTXG~*ڽ"Sq/xî'S,$!x x.3 ;r~oјI~6%+ f,D`!c,,cv-) 02x<aBP_@GPfeL1Oht+WЃ `KcnՃ0a+\oT ,zQ%gx;m݄bhf7$ŗ+a lcm65uH ȋ{fc8{]b""4QJɣdέq=\4jo\dq38XFo ɂ aEhI'":Xv0ˑb\ cTll.-ti(qX4Ƅ$vtP q4Ӹ:˵C91-iC@^(o1( q!fOT^`ɮO.fXeQ'A+GV#ar$+Eyt>Xs>rJ)m'BEJQTR;a?5m\SIPtz>GP<)<;Y=8c>m j=ϖ8vk8>c>?y!=`c?)7,sA܆Hc9ߏ 8G:n ѿt $=vomg+$titLE?@vNv0Lqʞ52Nm9dN|>kwwO`#8f4uFcpxѬ>'qs&n`q.;ߝv9C72ms㤲Jh;ꪓ?gϯ>$)aU!ǎúeu 8fCNM؍c:ړ¹ў:>DJ+D%@ дHMw}(xU ,mo?<\?Cѫ=acr9690+u,^I-sQ:FUItn08gIȁtVct9m/wyΏ#Il3v29A9ff)A6ۣcSA g8M4qh 3ϙ.dь5;FS0S-MHF-Vfa8PѪ+SVĆ 6d\zh/UlN`yV  0 k3iM*ې$%\$b{1ÙN1>xT7D|˛̍DwCPdo7'v?@F?Q4ݔDdOnO4nG7&FPqx<(rOf p71?DDp5jեVDlBkJQLC="ظldp]K6uZUnbn"HL%0%WҖE_yͶc)ku =m[eS_INXOet\6}$KdaW :H FAy\df9źR*5YPS/uy̖a R]Y3s\r? f?{vK4\2 F [ mC@?s94mAAƆ2ddv 6^ H/t^榦CPM23 frQpf3br#330Iɠ!qJ53dCToa? IϏ.ḍ~QГ4瑪$|R;Q#3`FH&O6@O6q7gzxa=<C8y sD0I/`xs371 8``rcp0x 0qGQ*#ǜp>AHD d~X.;CumWUdU((*DP)DQ2L )E*UKj&ZZQT#T)JR¶V*["jk] VU*qԻqOsܪڥR÷HMѦmú;fF%)Uhp(P/,'N`<?):Υ61Lñ>Ӻ*OL2vjӳMHki ؿ !ZQ,X'y8N$NRH:Wz1M6D1FʈuNT`;;6xn7gk)k&kL wS;I'y;J'nFu8"a93"FN)0;wC\teK>;Nګm?lnVMwvDu`Rki{Cuc%'' h aCoǹHBo˳Np\^"4.ID6$ Y~[SJ_[<CQr9#t9CY볏zӵ y~h_DRDO N$Kܻa=5ū5Oi-68q!ZZppma7SHHBs}$}u千`A_!.|7 fk % #fcv4HIcNLXsPq I `,kX'} fZƱ5FtQSchہxd PgC QϏL \fa _$02Y@t`}`,H"e x&t%Rl, #ao+$kf060 jj3ZAq 5- ozjӔ$za dI>1 0Lƿ@ӷ J t)1"M )B4jHdJ?} Sʕ8eMrG$.+ibrD?xi$?h؟Яl}:4ㄔPh.}R'xԙ9 PkR~`~ I-:4d:ZjszxhƴJ(HHZ"JiO jFslmT> 74.SodA6E>vL6Jvr9 [KގoXRSSOW-?r%`$ԥX.-(ؖ-m)PCdWQ1iDR2p{;ÝNdnr¿S-BkF} GfS&{#7۾mW^$#b 1-Vl39HnNxq<=mdxw MYsnxSZA/ڃu4P ^@-iժܓU+Xa͌ W4iRl,~K< k~;|uFC\h't‘0.5)mL%&<=uF3h5M9eў, 4ƝEÜd';HI^ ѹ7NfμKH^8 wj$~7*ڪ|b'ܝuy֟$0LcMѥОCm %%YN8!`("Ҳn(c0ԧ,/Xr^99tCڿ2r/7.n޵ UU\ vrp: kl xz&nf֛0zI4SAa p{h,IJJk1 i#Otk!tI`luF }ԷpMpFɴf+#3y[# 02 ʸw!*nQ1܅ M5!?l6j)ߤ174BYmD!'I? rl 2-L&fƖ5-pۍu3n[nְnUWaej \T8ēAĩddc[C""' d-ahVh 0kƒXc@.qzgL(5%M͢j6)Mᑷژ @A8=)N C|t # [ ))ژSsH"߫-`h.CMŹ`5.[4k G6F#3I57[Ff?LmlMͱdך Xrjפhĕc~-aT@K" 讙$"jH6&lwrhqM` DY0$ EPX '9e7<{fi1 Hc滋%Cs 6Qdds8{kLd$dhQX4ܲJr `YMB.& ?$6ZaVӜ$Tc`8R)ӌ Q cŽtRkcOJl'G'FMFOŝ LDxdry!ɎU5%Mdr#MSIeO8St9&N sFf[^4MFYic&ff&5pxclp(h3Ȫ YTpx Dn&s֗kk"br ZX( .|YA*f siP9:-!P3~4C܁ڲU17RTm dP!#y W0u#)^}XZ)N^•Wbpv#>ݛc Jp0nJp'.PҘ."M 5AC 5 Xk Mj$3&Й$?[+UʕO&~)/wL@NAcQ0VR2S݃Q!FɆblYj`7]D8Q B3 { ɕdçIܳ\oPMhz[,iwZ !de0Yhoi VA ApinCDmc!k@ibP#bX?n Fd=BNvgwn30o!dU5 B`.lSp0=@ڂ.h`k|j  ]bC뀛fo䄯Pl8riM`8պCݍѺ5u4I9}ҚaKCb-+*dY@Ԧѭ8Lg8j޹ٚ&m;c Rhk٥S cT8IF330ɮe3%>NIX3B,"b*NaA%!] LоW, PlK-mFh25VY6MlG @ 4ͱei3 3Hۏ55 3vؙHlov hO>VP|ޚ7yM8#D͘:'[3V߆aP-.}ZnhwJ`drG,w{LhԺjQتE֬_0Z(LlmU֪UXU6UT(PHPH@y>C rg782;5q-"W?T Ah-2r`WccY |Vo[n)* ]ʵBC7t7TQhzxIFx)di-&)3`'A=n/|.ښ9`P l~qxh,=7fR qgt#66>|g9\g8M[upLUFW)lp8 mwoTQ7p`x q >gf ۥe)OғqNKVEW2Mٙ|!acoL/H{I4iG# '\8=^h,+c T=s<7{v+q#q;yVF=XUr~D! =y%[Okl`7Tl)5 Ԝ8ѧ =?kL̹tE.qU/Yk)„ C\AXnk"Xx dFRT@VPhm&lx yxƢk !o'4gЅUT-\p FZ8Ճg0q0S&|=^6dch!0yxc|l Q52( - oAK;CRnM@a0† !I P5a;= 4#m6hL a5I &Fi%)hOן5`6Fѵ6{sLT#o# M#pÛOV>G\ 6)mI(47ɺ_ 3jTٔl&sl8C)mFF5br{A↑4ܖMv`H~Pq5,Bg` tQv Ƈ9;!4SjS0p 5pGLL=`0fJ`\U@;04k lm 2(!ki]Qcfi5J6!xg4tsgFfSɚtMcAf̸I0aMvљDԚ}sh7|p;@;7`ɔ:;ݎx"͆?r2jf5OL5I hܳSrn&,38cJow׍'Vڙ6G63X2m7ג6(,}C=0q :"Mbue ]4lK4Fg˟r?p4ƚi4ƞSnblNS]6fژG'ƸۥA,5ӄABjڄP& PLSM5B[KqPt.x1!br8Dpohfl"r\cQÚ2&5_3mn&h7K^daRDd`5r4Yel`lLsu}I+A;P9Mֳd6"ħ +H>dFJԭ& V> +oѤ 4  5,fɚtw{7 ՛d`ۛl59ndjFTc~L)RZ!332Fu С:ĆVjF ܆M`0 vq5 š81 6 v eL[!6NcbQ $ P@5֚i8 ;:M:AkdI36)fi)L7MdɆS +Y9al]2uJP4dɒ%-V@O`8sd8ߍɭ6&L L+loM٣#CRd۞A 6\> `fՠڂZv,`kgz8 +J)] 0\Sn&16M^o %(fyn 6 ? ޑ6fHٚfY'W hךϖ4"9ͺRnSJm%)?HxhM&I),*& ۜ^nf6G n÷o9 ;4"C ;7fw v9 u lTMFə8fҚJ @Xu=95 Ԡё,C&[M9923F&6L 2he)֛%6jk&`5g ȍ  aCFFdkbrPycn}a e wG4qCyrY0&EE0dD9M֍v}o[d̡ aaL);u=OSPTYVT>On ̲ +QCTC!M).9uf0px]1 #+:kMpffc#ILMfo2 vl6,X CX@5c\ QahVn mAFP<*T20lKcR$7[ACǪf5*j&%Mj*Wl 0M9 Hf>at5xuhcS K&c\SdeQ^i5Zjk&@( h>,ShQ{]ܪjSdn 3CXf:l1,0yڅՍCe)p=y a/e}1S˹I]p\Jx2ͦ5R&YN,xQfbb- !Pk5\!YF&46L0)55;LO0W<m,,7j86th[,=n%ôk&Q%8YN$7mF۴Ms&!Vg6ZaUadחAq0#F6} g'5=2G#'d 5sndFBuϔz#1Xqo7,eg^=c?m>͏en4ej=V;}Oy7u~S]䞭qF˰wǥ7{g+wþ>smO>@Ir}Xya$9ͧ:L & ``a<6}nޏl )&6A;^blx8flPvFHF1 C2aQH60e B ~,c?xzF`cFDWtв'X6Í̬[qkxXm=ɹYUVڵe4-m#i,=8eʕUb,_W÷upuGCo7> #!73i̸Җ[ *hXVYW?QKpl-.)w?}Gbi{]ռ A~i`^%9OViiۮ%_ 0Vפj?~`G!H$￸8b7nn\GdIT6ÙyZ&wV //`3hjnW@ %nߝ[kAIDOGd@qGq}@DD@1d8m.,7_M(_ <#0b^Փ-Zg~0BPdU%T7;F#N鄗苔,xɃtj=.ݮl/v=n%OU;|yC'zCnV<0 7.9;@mH3hJѼ #fI~.yPoqt7!P*IrܹkZ WW #ۀd4hFrWa-}3;5SEwahc 2{L EE@s{s?`{SO?ښg>z~I5,0!ǚ= : /u؁o;CfӑBM6Y Im]tRZQE)ej֋HQ[BJ/?5HP29\.>'NMEzY}3r>P5 u\*$& ApV}je0v*@Y$Y0oO &lխ'DD(sǟ?GNsW96[qd'8gCV:3J7GVAPA ⳕ',r9pCDxGO]c<ws(xI7v~IOX| ya|^n:aW@<η Ӽ9ܭUbWZ>q&,ETY,/η؜VR}:yg~!<\=|NG{Mi/J2(F ׾Ƀiii.ec|R)h el]_UYjח -f2[>oاb pSHȎ Ȯ,+ m,"WN 46`a&М)'@\=WO:u'T>l vC:c4P>O+;CH~#^|rG厘8S\v7s,Ĝipe<O&uG<9̞,\@BYH8K-p{oE\z( Fލ7Nke,djѴ4$YW]uʨՐQT |k&Ͳ>=OÈ5Z.ߡU{o 2gM lw@k@@̔DS1Il``4TMٙ *d"ѓI PȐɱb-eVo%;)Ý,NjW \t$^RWǦLu2LFfi` f6R!+)4#JY%7|5li##Ao|<p68"Wp+a2=hdc`ڼs6z W5?16B4ijM%T[[ ~eZ rW9]":k0M-]Mc޼AW~/ԩB+]d>rrM\+޻ Iu}&V('ђd~!#G+ >>k rRR{uf㍂s4o+UKmRF! JDВbD%)2H"2E>wR&PTj5 0c"EoxK;wߓF}k9;jAʇE&=,duAû c EJN-Sv+HB\`|u]\}Z7{n<26w'pmdud+d;"1(:cHoA yx*_wtAIDA Vv}.Qn$y΃!;7)C*C@%-冠~7O3ה$mu n}o9K0ɗ~G0΀zAlblYB>pj#%11]_kv4j?%'s.[lW~A/4L>h蝙{3`Æ'r<!= f:I&v=7nsn/R.n&OulkMnB~#p݁8pR'2wa `&gU|~>gkv[arPP]PՂ8VJ7S/OY{bh(!fywpUwT` D+$_=%&RXL)e;#W o{mOpͶM4ᇇw@$V^6m/MO&°;t锛-W~q26qL:i)Q͚7ҪVsGS˜$]u7$K#NM-UD4tt{+_I&y9D<QlO8g+JYד5Vl\f*lZ~06F@SzI(Bbv7Q.?q8LC>Hr’+n΀l,Ȝ#kOPx7Mퟢ93'O9]N+xDB?ų/Ϲ Rz L刂9 -oが7-ߓ ziW6<) =a\ X"4F~pw]Z"O\"cC2(-F~;;=+@oα,#_v9tef^hT%wl rfB4%v']UZFKvԋFtlAAD:PG@T1Y}VE64M6!D/5;>JUmy:s.a?voff?\NLrv)Ub̖Fz.ӷ'}6i4y K3S۷`,W2ip9lR^Q#elPA c#_}=Oo ikIeϪm[gIn1Ak7@)%TyS>P|W!$ ,zЯa;m*Ye&_!'N۹y<|Zn1u{LJ oٷP Tė.# f?lb}TL*&( 5ءjQQ G `'&]ae[-Z@ ov7fU>%w&$ f胅!)+ɎƁ~P Hq wZwxl 'f|: "fV_vtgEF Цq6t?T=/п?'٤cRptpn1Mʞ('3 bu.B uP^SATk˸] )(#)H% 7՟`S\MNE 5v:::$.\ǿcپ#-5)&5|K%챖aم%6\wkk`Mvxth6F LRͱCݍ}JTo7Mn^+~`H)$r8%As=@OONO4JhewL2mr`pLL͔@)DJ)[7!+vC;! 8 -'&\ it24 Çp=I{> Qh-1@ᨈRm4Z4tX<̼eČP暉qW1+zt/L=A8;!cB@ڸdNwX9hH,%Y+B"pCCЈk}lN-? YD1E"1b;-@)@]z&ֻ˶@ pr1\B? W\oQ kЅ$A,rp geVqCnLǕsO>de8UyqBx-Zf:܍MUo7jGS^̴ ]*okJ5*iW%3SBmiؘڶCs6Xc .ቻ4fV߀c7Om{jI`:`0[f_D _ s@3)-;?z9vHx !y!}q@pjk7l&'0Qtl d؜Yŏ.iΞ(8qNfw6TzcϦ9Q;Y6 ;NdS\df)   a ٿ."y6q>4d l&ƗM[Nzu'RltꏊVËy7vu\; u0aG Ly8hE)zg kM=cw$;#P<}68%%U7AběnŔH@mb4 NgҜGђAۛӊs.ϙM or{7?ma1^ĐxAkl_I6mhZKFr" zZꀃl<^6TD Y12\Ň Ri5fmmCt% cڍM:1Ɛ.q}B.UH3!jj4ZC=E&S|:Sm8N41FNp8/-%emoA,|u [+~mޯ㎃dJQ uꀷ(^pd ]@A\) 豖y-s?)uz臖D\2N E?GDSO,iv$(TA)m)JFDW 31"`۬%'1Yoln1Kc;Ʈ_6@ބxs\O2mܔn:nۧo|)yNM2|gK1oڌxfLP㟸Lg{]Q6LSEM5˾X}06rӓe-_ +L LO8WyMFMZ޷fV~cPtV>hH= >;q~(ZZӌÃ#}7q;Пssu.[O q*"P}0e{4U>ـxAFo71'ܓqwlj Ў1C>.|s7 p2 ,xb % @ A))|XI] p nM)V<{P*kT/ohH4 M4j(v$I*nn`?2 h I%B3 Do οU/0!萟 A:?2ɷ9$T' &TOn& s>!3<ܝyH ƺSLOTTߝH'Ʊ>!7tB8SqMA"h} iN#"= '%oxhqɹn>w9VeeYm dI# A D̈2o~S}ै wiX8&@fɒQj$i7^LUm=LUaW&5dž9mS{߃3Fͫ(Zzɿg16 ,m*)S0o_3{\=yV_UfK?t3Y5>{ӓ:9d v;5eiip$cXZ3BΖ>Ph eaUl/:ٟ Y1n3'VjflualZwAc`U NpLfΪjrfx taSK 1ve`܁o't6xE=cm!Dڸ" }Z^̓rrOB~ΐr=c֛'iΟ}yڎ!gNwӆzl|觃3v}#68u8Pf}7>xS՞rsrrgy;Ȝjz t}ct}Sަbr  ۫ᠶE`6k]|9x]jL`ӿj>moI:٬Gj50dd<_}wH3!њWYsD/36"$]y)fP/wD:p+km/Iʼnkm=>DIOSN̖ذBNl vjHk`X 0b D8m?ųL=&>}Jip YS\<_`U:nbG~; $6sU =oo@@UeVϠ}RIdH2 fh$LF$|~ƞx'G##+lS'/ջ/|.$:><1H}pgn\rT./+%Lgc ZXQ70ilL-ڕbYDY !I5h\. _X2RGjmaqb*bTİφ"אDE;:&MP_Շx{C)D Zx 1ωbb*d1LU*UhEz RK# II fɰh&͔M M%chش&CS"fb6lojױ&_[&t^9bc)[NǗwt:O }j\-UUlhMQ`AyyH#=$=p! {lJQmKwqsHnc \J?Y:CgfviS)G!7$Gxh$:nu8ͽ^c9(fg6pT̵F>sa 3mj8Bn"I>Oz=>^ݍw߼葄l+1Ӂ)`Âcp ;8v1>?'[Ԕh&BM@‚ *) ާ@,32nYccc#t rȏxWgUA4xoO L].tߒk D n6p6[ys[|*mmhl`h,v8o |YYdZF[Wl,nr~] MZ6[ޭ0i4gޕ .SI@0Xb11طz-R"X|?"رЭls wn鵜vǘ t9y3X,yۧm8^QgH:6,6^!N  0]=_~¦=~Y1nXڜiS=1{zkҐ--C B95,M{Edׅ%/Phv| Ugwx_6ieJ"|'`R34F ˍV;tO{*|l&!lAFҹ#fy;eخaqʌt upurOב>X>IGO,FXcB?NφD7!GFmv/o?w&,wI$eDR#IRF8d!&f`<0יxd"0K'>0[1}i$룬BJS!ޖYzOS$п"bjeh'Iݝ;Nt⇭p/gN(PPe*$~a~db/|}>"z/7v}NlJDcDJݮw48-|EqR nH{Ods,*swCS|`=aTU3d£5n񒣧N`ܞS2V\SVC tԟبЪq\ ͪ珂aT^;isZU*5S+OZ]y:5wTafM~y?#ΧNB;锒MT=X$uKMrBmm>ݽ'"NPD] R%A|(ܬ8۫z%U(&=eMF6nh " PmyUk):&92g}Ѳ&*`v0(p=}p'zg|ya{ގތC`iX>`#Q>X~oy>C̼mqӣ~dcKANRa%:YJhX0w|{E~=Ox|؂`@Z87{؈6Ep.L ݡ*6+ qqei&+Y$#7/|ڿ?/罳nh扥NJ~-xv[0+`nKjy1{K.jKܯ^kۯnn p,Wg3sL٩ni,ޏ]ĉ5h*7T64)@ACMHf$oወ ?~.ҕũC_O´E挚Szv 2[1) KCΧ.u&D1>hm*6WT|2o |[}͚w՟X{5vnϖ\e|hӪV =͚yi ҄2 n OzJRptߌa [ Da}Uuz餶F߮>I,TJYNntz?$lMn WX~ha_L$jP'2Q^>4<,q؎Nq@NrQ-t/tܻY=@dtΐ#Mxx:U*Pb~*5_-+:ߧvO9:'Of?6a)a Nؽvi܉ؚ'^ $ @vj3[nndHd W/J2f$m$n-#lHyM.׫h'ޙ^@v. (;3 @8U 4vzZj!ƶsD!s+Xv,#*N9G3Lyz:݋t4V3 XjףM-XbKm4m wh\ `-!P.0)yCQ unִJ×|MDž0T=hh#oM߆h}b~u\뷻"԰\^TcC{@'H#hF&x َP`F,3̢FL4ZCx=SkTSQH:??S`=?m(iBBHMIR&o$0xl6Ym`cDUHHq v) Jd,g&L'&%%Ɯ9iBx6FHשJ'q'd$7q>Rp `7CM44ƘM iF;q-6rۣFi"8j(PSSgbem$2 8Y~4GdHXqrЧC8Ʉ&8'79Wqq?ދT0ƪ5B>a*N4mn׍ ƞ5KDX^cTCx#r|.:ov&cbij-,.j A]\uNYǣm^v$rC!!DRRXXr:g+masi֞12ϟBiv`Fw-6?/AcM5 ЩTc(+؝UP$y:Jm73ѫip Ah`' qMGH9B'hNxar-V8["BKxT,@q=@TYob;j#M,AmCwml,PɤDQ*IIIII̹ά\eR$D66L P9Y =e''(!tv\Oi}1ܔ$Ha3TR16Ν'p(*B m1m  ;lBjĘw@=&<ty _:D.`\rktҕxЎTuiccoOwVϢ$bSO8j (2sҮ4="!HQ(eF_f' # x YEŒF,eOeɑy:ƭTD1HH6j ͦH, gQa A}| Җ4Fϖ,ʝsäj|w=a  yU N:wHD @3nkqf[xaAj=;4:pf`qwT ` z0=V{x& EbQ(Px+[qo }؀@[H`+2G pk18$p89#FٳmmRdxoNP9YV'4֖oR3ꘃTI^&zp5|FUV܆xe B`@݆ko뺹0,aX^!(evpt¬L8 nN!`hxs)M&n()M֛ 1,J%LؒQ %JG>I9a@4AigmCq!C6MҰ»㜙ui z"nIIHTEݓ`^ | AvpÈg eCxrrk&yA5 p*aUOT582kެ°XS 0VepbYΛÒ7Gؼ:m fF;Py -ڼq& V`fr-_4At4r< b_ 5b UGԑs3ܻ%nFo>;!2ށeUma_nɭL/wg5%d]0 <:`hy@5el0H8ۭl]dq&msXu49Ufa=#Jw4,r\Ӡ;PڑϔI!&#&)nӗWVy\Y |Ѷ) W@ Q%s.Pl܈{ h\h/HP.k[EMv )):؞8pxW 91'uRdMo# 8)kd ^tN! |g12o/2{Bs`3nIMܓls=L,)N70]>TigҎ |$%:ӅihdL9'); Hh>u1&( l*TR?uRզg"y;NXݶDDA! 6c6cA^Y{4qpԈ,TleE66 VAXvpg4<@6؟u@[.W f[8ϻ{d! K]WTg= %f7ONH pMFOپ[ X w^]8r}⁨UǸ;HY}n4@©2:Řu&3NIFFL)Mʳ&#G=R0 >}yVӏ42*pF[Mn{j{8ntcfߐآ8ߞBht|><!]]bprF^hqXg1"_GV, %B9tg@Gir6Eit]gIY`>|D8!s.Ɏ%[{]ffRfI+tYUvf<Ÿ< b8li96y *9`6qdiB -5Lչk;9 νxO~d*}O*wĚAΤI9({R9)J4r)''̍jq,r(h-:(r`?4=pjS-Ȥj& +` ) l !@8p8k xL*Y-%HS, t-Z> ~h8 %On+ߑ֯hf_3V/ɶ4_{[K0TQXlbPi1Fdɓ$ZY 05EMPo/[9uVXhn }*kt>1װ` gvevtn\ iiiY7/;q썝D"C`N2\:"(熍W}/.˅$d cA?M7DNK}ϳ?E䌥0lSbgм, yiv_( 2:w5shya[iV˽v 2DIvՊip U'U*i|4bq f@ ر^6M,PgDg8m ݩ *iAFq{Dv'TP{!d [P-mO #Yi"7[2<l"NRjY`5Ů7D'8)xF擿OHTxɒl8߫/ wUįx9~OFœXp(p8 kƠq)ji [\2 GIm3Q|iX ӱ(nó łC"b[ۧ}W&$2$Mmbw{sf. ED aMM~F(BYNӇX͒pSe$^"pPI51gS.}H㲬FnElnɚLz3ڎK6ȓڛlm+Svt\ƛ$, @B ioGlmYdRXaTS!X Tj(f6l̑lMl@؛Cop^j{@tvC&m 0+wg{Ftɾ&A8 +'osz݆5Wccz*Q@,~%Me,ݐtB+ ٢rÎqw{k@]dM6v sORnͱjPgUmZRRWSY5u~HoW}ʁϴD.-7[]:$` y|%w?O$$sӳύIEڵms^<7 ø盷'M)RJ,89PhEVs""+t28߮dɦ@y{+L0 g:@/0ˈܭVU*33k`ˊ2Qcm!ӷ̎lj46fJlH:`M(d-+<5j~sšINP2TUX&;U+~'EdVH@ccc{X`tf@fȚ{cD|YSM>Kվޫz4a*i{I5ڇdX-&EѦFf|]r3%O¯DI,2&l-lmZh/XoHz?c_jWW2`&H36R#( d& &LťM6C4f2&%$gOyvNùE'B{x7t PNLI"+f61pCkÿ -9 S\u)yw0Ud'oXȬT³T 8O+BѱD0^k~8*& %I)))*M`9as h ww38{DDF*׼c:l:;é㕐9],' /nԖhgY]/C,sй1kmިp >m)%/s 8N~O;eXVuSr2.nF6K*U&3MC၅QPD2L-ګcpØtsuk X =U{+oo[%%$& +Q25mxP us/mjJNAc7Fv6qݍ2}(hy#133+K zKD1eT܉Idɢ'm 9zXmz-=q(7!N&ǚXpzLRmƎpa}Vv N 8oڕ62FȧdvCkdNtO[p)].M{38Dl>QQl"0D@YVIdB+g??e 掴DoDyށBfx593G O'g3[˔y5IS1EJq "6CoDAKH[geϾspH!ɞ4;C:2hij[(9~<>a.fe}ire{ʼna=?Lۙ#LpX(9P)vwgˑ5HaKrL,vӛήNS#)(vҁ" &CocX2nu]eY-Vz=2VG*"""0X`1bho ^QKԁ𧻓Ǵ ܃16-|Q\smm_;Әsn 4<ډBP' {`!A耖L` m/-2$|cnJtpZ`*bb̏2qE`t܇48 i/zTI>ti=!OQU$5Ҷ Bs]o*#lMr{' È">r6U6;r _VpL]yTZx H^δSɄUkrNSKL!*}T**)*RR&RN0I+5)J \B1i;@v6&N96Rk(Tvhd ^iy0hNc2=ɈEk| ]|[c <)3FGT^ Qt3as3d6IL0Mc%Å6ZA3C@vb3?ZW Y~2',b+Υ )a<§-Ƈ|S|Nە\28.TMq)6)Z漙5?$9RRdi@MbreeV3'@m"d'ß:v-rk^Ump! WE`bJ!I}ɉN7 ƃO^p ; "a0޳4GtfgԄU~HF~ck=L_]lk8qw~O+\!J䓌q3?sSSDZ+Ɣ}6GOXͶ:ե؍`݌𼝝҆ )S3Di͚PG!+/,"\>ɶ Pz @tCdo#F|j6g)R˟ wA'\JȚs6vJɟ)4x yZkw-sGљ"5@։#Dsm"QD<FT"Q5 }`rԃ56|z`L$zӨD;" )*ם խ7%20h:G{?|tgF;#3?%NL ?tC6Iz!y$;!#7)h8Ebia4fh2Ue)XZTXij2}A;yQʢӧ b!'LgPD^ubwt:c{: <\p0rFL7pM[LMtuNtaaziKiX V HF1o>[c~Or~GZ5=:oMǝ;{'!JǜIJT(a*)R L%L00E,YeRW[uu# L& ` Jc] T\C3ZJ:&?%-7 Z)u9ÉDqÝ.)ҞȮQNphܽ6sij7ۘ7ڶul@>Ez7>=sP: G>Nɂt&ЩNIJR@j%9QLʅJ%)$̑B4)QPhd*SRQSHȘ!RLL)JR0RP1 D"LPIE"(I#E0RdL&FGs)SByi $UH #)(0F1$hJ\MFF `Дd)Y)I͌ $3%AR $̩E"l`i&F L#Ba L )Sv|9p90sLi43Xވ[0)IUps2pI`T~)dG,pxLpmJ2sn#q7>6 s&(24I=jn ,&gZ0$ל9UzGTkk鑴M7zUjo<1n=Ck~2'e!w2~{'Ys#7&I.Ǚ;T Y(WE0Fkb-14vC;[ŧln-7?XS^ nӕO9hrSLpAnߤwqs$CPqd4V`ph{AUU][+^jSQT.]uwT"f;&TEul<~ 1a5^rA24kt!if]3f 9+ߗ05HfۓpBNaezm ԴJ"/_pDtWn9dz3k=otݔ}h)JN .b46X *:Vjg:\D`4}uMЎԈ!ԝL 1 1vFojV DI)"B_ʫgX$ "(R`0`4:Ȋ KA""&A쵿6ߋWiHH|zOI_NQ̫Zp'Q?yOj&)sKe:̧2Jr, ^郯?Mv¯3oۉi |mxџ'e܍\$e's~49#Sa5Mh7r3jT=[#lISkvoMg='vG?9>~yzmg\|G5yd6?hgӔ 'wU+ޱ$'87S3 v2"?=ALJjdfl{6YdI~,s%m O;ۏms( Qџ0AזX\02I!ԎP}1Gv'>^$,UljZI3ms]sgr΀wsgvMؗ5L f~Kchx0=jH2XĖY`1s|lX|G! 0WʏA#ߎIǼ\'!c$oΏ~9Ӧ"sJ䉾qJs#±gC(\"%S8F$rx'n_vګv>6<؞)+AMzRi e$2 lc?YT@W P}=CaX&Uj7+KEJ~P[>PU)aHPI\`u><󯟗5Qd[d!d|w?-jNך$bДQo q؝9ώFwcEO :͟:tȌR=Hu6S B\ϔbw#K:6vH21?DaˣR>^ h{`'ٛ2QIϏ50I$1p~^c2eh.Qi*TRQEfR :5s\s6sr89u#Y wds2ed Mn)yLb2oL63dh#F{MGT"tG37 :`L:bu 0;Z$}`#vO|Qҿ-?~;Sv6eΌN0Нhh5yoosNY3dL1ފFJъIQ~W#;Rxmm_fgR#TsC%)tc2zaÍU Hvy5 Ɖi1%>O6\yPsF_#t֔wDH?qz8:gz@/+|gwUiHڛ8hFǁV*1WSi6F 勚ڴ+|Vݝ 1N`.766hR6hq41,k 9X#9A}3*9JS,?uߎ='ΨN6؞t7F28IcǜG'pLC#5=ؖbIõ펵{ Tu^h,+ K"ckBHޅn|]T D8cm_dVBm2h":hyПVv$VaY#!UiUm3`3eLt'F+~lZ_;Bvˣ9e9dMO GPѹq8T33`I4b#~` _<(QNwk+c>hrS䧲J*TS eT!CG(~A}<4p7 {psbM;x%"4 €Ӆ<,t(k|@Nk0*,vomy So_Fn2!M?J}j+bzURoiIҹCnڥh3d`FGsDbFVګVmt6S$&ۅ}ɬk|bUG }C018N0m'6vR.4 T Alxz_ASۏԜHf, ӻE6vԵ^r3VFSe$Cٙnc~;sϵ_#\ޖ,҉SJ55DŽmC9qպ/ :ư%hF}WGvjUh S,`}nͶ-Z`)h݌ JLp57J0?S2UTKay>si͏OO,`NH(Ӝ:} DLN".M#МV[)1ܻiɠ:.OtRO}o巷i6H{۰S3azBfL̒ª_80e(A 0 3Jv~1_`:qNkrW>HB*{ :ě HN#ֺ5\qP@ӿ750֙N|9sq=99sNq2W:U3}>a)hd*y|nn{jZp~ڱoTPf `fpE 0pgЌfYZe4}RD$d_7^e7`929|B?'p]{֨ι5Gݍf͈y$JZYy>}eT=I9yvxڠ9iKDsS7"s G oaABe$:uL iNUfj:AVVv ',96tCHQ#GDQ (؞dأ:_hG5  Ġ)Xmo4oi yf)ym큭+WĞ~t}?m9sə|xLC) #?3kHDO<2i?¹9''?F"`2P$dm6Ϗ84#KkG3j|1n[3 l,ۏ'͞NnHLq]TնUL0ܞ4uԑL4<) H~0NdJo;A0T7#~w)Pf0tSI(Y,TX~, 6fuJꚛfMY%`S'ɑf`beEb0`dLK2)&)Ie) F+AA  c   `id! ,B iJRAM i)\ (0 @ SK `Yd`)IaE 1%2&fR2)d)eKyZRHRR>ɉ2J,)-3iez[nͦM)2CJ,a+ĠZXaH3@aH2 @BACI0R4Q)LQ EfQ3)L (!CJbPB"}%6400)rQJYK X*4J!ad"Hc.>=suY~ =cm6Z:i(JO܏ $vӖYaerD؎fTT>)Jei>sMp``ZF)M1 SLKi-#f14уiSYYiRךb0115hO?c+Zb1h/-M1-RRRYaKdVAPDc^2:`1@ `lfR*n`e3-ጂ224)}>ݥ/JOɵsv׻QDZ7kdܷ/~dTRK4u7P ߂Z-޽z<g3<0g?o`Y s]{{tW.WojOk@B#'DMfɦ=DPR /#A)]+vEj1`d( tIƞf?0J;˕VVN Ƹ; یTOF)Ĕ)HcgY+a]vDx p`lơ-YQV/&5'}h5j9yEgd["(ФgSDXUz7'aFm{VKW ;ɑ)φihVHc%%k akwbk|V_?$715mZ湮kz$p2jQ͏nQNKlYRg!:QK\f5Pz~=/R'VUQf[ܟ.@v]1cOF?؁tqÞg¦C#&ǵկޙu4,iU֚Ufѡ1XB1>'L觙qUz2?)""O<(kwkK*AJmuvnwnVwvwY:I;D"Paw.s9nTeTeUO Y%nC,h͸` Or,||AYNP&>y*~5{r $gt)KLƣ^֢ tq,|ᴶ502j5LZPI ;i#qk0iD1>J~)=L;hY愹 6fl̎Y,e26jsr#\:"4618Gqq{5$Gdg7bZ2abƿ{\4ٍ~YE̳Z1m?*n!14!G{ E6wqJLAndI2X&Ajb)csݬkFA>ayatqeT$%P8; [?Xvl4jH?!sj{<ڣ]C(sl V윖|C!8{7#d&Idy m73 J1# ZitAsq LKnǕDKQ[X/h r19q !4Kvlfdp~H~YUm#ņ;8w*&*[0Y!?6I*h;+cǝȥ0 Ԟ&h xw4@qctjetߑ5)"RNhRN\(z`&[VUff BnkD g5 #QJiڳJVJQ8I)Ÿdm%))I8Qfc!94*? kҪUJJJRfT!20T ' t*԰7Bc[!ř#~~Y!o_n,P)GhR_wnjm3 dN #z 0hd}y@E Q #ݏT)PrLtmC@TQIf6ƈU-JS՘2dVc;f7Q4SxSQY&2FzUJb1̜&%E'{wnb]`2OtkH.WHbDA0Ȅ5eME^U8")Ϝ"wIAXiUw넃wwohdjB'e7ofO eI!JLʒn:oJ#3L]юJ D䵇PG&`b6%<\iy?)uxO ;Bm6*T8[ȗRtʹ{P 4 Y0g~~I]E-`=P(.߸l'UmmWG@zHh[-b؈jQAT( +9A^079Ǐ֤{?Pﯲ }j5[F*GzNpơіv" f*R:$d<(2y^ 4KCcbh mk`<Iefح fjM@8X6"+'! ]Xq!f8ILq[]i&6` 4hN7rB@ʘݶϘ=2̇Q h5b*}]0H@j)'[CUr΂0e2HLfNh1s c'ɻѻhΚK坾Aa۽kOJ&i I$iF܉D5%c R׈ [wE4ICH"ȲM\}UWUD2/`ʹ13qh M -/mIhTQz$ -eC"pa:ΦdeSXCd: (1 kSHWР`Q4Le U6}Y!\8)}\Y RR`~#z}u:.*O? R02Hk r',#!6m2S)CNy͇nnGPUV)^8M0 1][ AvKi1 fs~'gݶ Ԏ?hJ}tŏ.7O.?Xa$=Y 9n0rǏOVt.`YdkJ`wɰح|6nE{?IEiw')JY'1$7qr⪾8W\4H&Y"o}Q:M3>p.1{z!E: -@ Rg$ȋ؆;!G gB]T"+Ԍ1 UT0V[ <_Y/2 Mg, Gx.zLJߓ6mg*- C`#:q[1#n~ e,~ԖdPJ:u48kSlڇ] %.+|1ٿ.i$*rb쎽;ۣc;luA3Lcȏ}SN*'@#l-t Ue*V-[gDmGl:e*Ħ %)'=KO=OO;'yseWoK)ןd3)JS~2220itij$aqכfmW~l꾓*N=EĕY`ACdv>lF h /zQq`dhFo2lemʇ[A67yodh;3lYeH(ӓ1 ':8%,KUl_z9mN`hEj<,' :[?CG=fC䣋"! :J򓁾 .ɔdb 0om&zozXJ_ɍ\p-|,`mRci+HSa i@ 4Q1l:a6ˎSnɒc]U 2d#͝Dt@smIA F\QƆ !d(^}9y 2) UvD 'S7(6@&!prs <E;7MPZHAS#N'މEm#8=] Sڰw=ڹIIȇP"u=L2muZ,ɕnJ:01\Axw˼$)$r1Ŧ n}d{/Ca¢[X=|Ab+ꚻ <[m=fٞKBcdYSC3 Dx[4=;4/fR%q~du;flefvF 2W4nв{$I-(+F S}*.Yj`3ہ}PPm-Z04V C3&IVV Bx }Yg{zɔ6jd6hדNN5{{#$ Lʪ2#1QVD Dr)qBMh"궹ZX屈*ikrmA*iJ--l?~0m]F/MCytF>1oCot@ךƙ]aJbA;,0Fq N8gPU`T(ZfNu2z-M =.J$27A]*чQY,% %]0ua&G)Y$Sf42bQ"oȅ'+uNDw'^ը)o^rN[T} {=P"!4i) kB V/rvp23W# 'Ƕ{j*U]哇1[ lN՛G3I8vi߇唔a3&79Aq_S|lI@J DHҀ 寝좦Y@v T6)uMr(\ 08 PIAɝ1C]zL-WQI-CZSِ7snL\H+'v`" H/P&beKQKv5_8\~h#445楴V-Z!I 'm;8);奎0ȴ 0 k+/M ͢Bt~`$Я!+ 5%,ka}ȩ`Aa1iKƘJ0☞a` -B1 J`tq@-yg9 _^G런4Ք/09@fvd95WyLΆָ.h^W4o{|^|ETRRLsu\J\}o '[I4?#q6dȦWKt/7z]f[fJX[^q37'WZ:}A֣ޜ׌yLс&tnbiaOB H:Ҧrm~bqY'tI(?I$;^lWVVIznp|NP:*Њn[S3|r>8<$)s 52)k5Cu2? h"[9iT87|kw 6'x `>2Nb}/}I8Bkl,Q1iY fg96T6D;S-įlFKLJOgߤZ ft5v̌KǢX^ 81;p pLqmvџ6. 45ɜVp֠ sj-2 aQ2cQtj @x~~-a2C(zPKl-H~-hhۅꯘ½o bp 5宦MU#Lq9m(ͥp³`xMhZ" ж"\. l2)F,!NϢbD! Ĩeljzݴ~J&3|osf#0C-\q0JWpf֤Rvլ= ;X唶 >SjU 9CInxL&w{uht ,Qf$fbѫm6Q)GxVh/| hDٛDcdG 69#??|]]01v$4fm\%;S>diM8? ;֨1,c2FGL$H,F j*!G:bۻﲃ]8nɟv M#IL6pAqYh4wJC_1}V鱈 E\Y/Ii) tq &M0 N@7"K As 4Z00e7YCY`P[FG6֝e ;Ҙf;64I Xfcs'vo6\ 4q%SMNP‹h#qZ@݈;*Xm l\S:z/kۧbZ' M 6<@qۺm&%IWµ,%v։xi71h19~_0:2i}?.ߘȯ??׷؍^5&Lb+fZf6ƂmhۄDh Ӿ#)/e=>087͋bX$xXio&c}׳EVy^X-g]*v)=Izѡ|Akc护o^x(70%pݪ 2C8ҧ[(v¼U4]nBL߈sVD'kx^/|nSHLZkIaK4a{CG`95mB``\`ao 25}3&rxQ'[$(`% u\w0a3H7Ec~R'mHN;mV<]ls;WYBw,ɫFY۷K[V'bp p5>vƯ'fxXm0[9p }'txB0LB ex.(R3nH<]ږ $;ȒlVq%} otMQX45k 19'f@av!ͭ7˪"ۀRq$yݵ GVTe2 m*+4ʊZYl514j!!%83C%HnyBqpCJW:&f|l 000}r')L%)ϑ$x37aCŒ'AΜrNdr8G'%ß{0s[=e9!W8vVնltlJ :1)thJT%k˳_dף3V<zwi$gŊB~o$g (!eX[|aԁIm"-)jX6WM Rgwlkl)ZFL>XG]"'jE"rQJS`Rp>lMZrn@׋xDjjmֆ0Rfk̞nc;}F$w:klLG:bN66a2ĜNл9&E*mf7ۜ3QBʘ mNPР7kXV"p[tвF)HH0 SU( a wxAW8^"UΊ:1%`35"QK=wh=4~ R$]KjiֻLb_.XP0Op1Cw [:* ~pԥCO)R5k ԧurՎSp)E00Y&蔥6Jo>-5 3yfR s(6aaeY 2'@,)W5\zotŏ{罛6X۾koܯͼj5z5 L `&!BYeZbQjp&0j5UYXMCeFHթ.y9wBM>&Fc 3T SA>0977 ' lݛ, Ƅh I$SxI{x-D/ ?&@6ਂH8TGyˁN e 5ϱfHnV`vu>#w{av[KD- ?5tl<.'ߨ o3 <φcҰs=8FCqc0JSH II(=X24 04!9W60h / wvLQ<A>3 j,%u_)*=s8lYebSߪsO.=|E/zWS8B;mHM 8y#] SDݍv.{YܴHbD&إ0,ƣ k5{xAÈwc'?v'vi7My7ɕBpG_;U<,4S1zƕ^݇:a#PN(-mS ! +@8Dry[YD'H0YDҖxss{EI9-.0ƙ%'; ǟn]63M⎽Lg7"(jeJI,! L!-gu'6t\<`p50O!Fci^~ms=OL@ K8#Qc;76>öm+|y!FNqL2792ߌVLFO~W6($*28BX+ Q2(>Xi07E`FT\zTCEEM=MLvB@b3)NMJkjUЦs}ӎܹfwwv\QHmPDݜᲚ ˮӻRS-:OY-EdYO#@$`h0aVڶà Ávg/]R*ԋNBPNƢn4>h{]` 83lCI: -wӟkk8{vK4KK:Ec2i[-S Md6aCALjF+0,"Ŵ5mpȌ@ ; ni$019eDQS34kߚK{~{--f<^c8y Nz䀗ufL c/_,J, }xu'_ {1*lZJ-cZ+ hi;^>F&& MM(pꪧB#Y9ek8uxB(m9 Ռf:e)!M}̭JMEu0 rND̈́U+V6VNP{)!1u:4D$Æ7'hU jp.):11O a&'{tɵUP(307 ^t MN%!3$1SxIrnMrW=%m~NZ69# F⓻$o*MZ0:sn:QĔdR uFFe) 2))Ϗ5{W*U aj FF y&}򣼲NL;;N[&RTۍߜFFߗ>S־ʶY$ܷq‰BnO'a`ց,!b@%e*OWvHtXY-،F#F  A ;-ءhnMUDTh45(4$T*QL(j6c #$!!Jy#@$HI,JJ"[V\ 332(sY\{ (ɒd>5[S *XQ^TR |E 1 <DHQy$%A0R" T1X F| DKiiH0FDʶUͥ$fTv$F*PD&^^zZp77*m*UK2R4:0S$LZYhg^TѢ!RVU*U*J-2 A@Tp HJuWU PiFv1R:e,af(`>3ɔ T 0 @9ZSyn<a P)j{i']ql#1gy&c aoQF>y,j8iڇt()F  0dd2`9.s iF^WW)%)+zA0c{فx@P9VZ\|DI&%SOg=>AwL_ -yNfvƚe&(x孅59g$z3' ?XIJ&> 92 c)[;x̥6ұjCͤ!l5[@3qOFmloo7oW;y+.iJq(]|* ۢ1iUE0ŰUUWee+43-J3)QRy)N63sYXK9w1N!=o/^ I^kHM8 Yv+iiE3,šS%1rfVp8,E(YE=hzaW}|? {AM{lgvLZ@h̷\ 6Vd7l (lSޝ JoFi,f1ԵkHFUWm,2)NE(Ȧbʲg^h7B 5E"DlL[ Q W\MoA!+$I6'5yi՘yb"h +LZ ~v9TyR(gߦ.rL )4̧bH J<)CeyO`P=N'mQZ 40&_}9㳄= *4&b23]Xѡ&I~ /-Db y9 0POy}Ցef8 JׯC,3ȩJVdqpjؓMH}:ejXLd38pAt ـ'Vihg= ʀ`"-*ަD4 Q"-^ cBȚdYn l0 GFSwk1,#%=0pEF| TR/ĺ@6Z7 g)Y22Ӫ)CM) u ^KhxspwcU]J 7+(msqSXcGI0}]wp-uUˏn׻xu6 -[i+{KtJw{L `=%SZrUVÈDtAkwK3)*2H1dE".'bsT vP)Rho(@,sa1Fq ~' )kX ~7AܩkŢ2{3 +:r#Y-Q8L94Ri z!hqL+Ov3ݎZ˜L 3yllut-)}#~#~*Փ~FOjyįr}{*0 Yo 1̈t:Wl,k&XA#f]̬&w?\Qc-Xvg.?u}>lj#2BEĉUclh3Xk& UAך fRTlSt7TB2^lkBv M 3UqфtG2XgbloY:Q!Tb 7$NkqyHsj8GBIE0giHJ-R@~L MԆpG>Zٙўȼ c$tc#>A8U.;歧-{vix9<>NC@v@ڸ (`>JGfTؚK|;jz',`̑I((:C)Lɐ#4a;E15-ZH6 T9pىܵ:=`@3l]0(JŒ#`_n% R%XEO""BTypȶ*}R` y(u,^\X2Bt ZK*i]᝺isZ_W|H#%%F~lYC\`Q0MdpĠ(PN117aH(I)FѵZYkoW+Ųb˸0K9t_S:Y!hf kX!(L,a%HoOU- oֽן3Ɯd)`UIJkn(8 UP)HR]9!ztmd(NFco$m qLydN~DpΟ$ND*&ƆdSNS-򇂊SҊ ԽDB 1CN[bҍYvm]anZ2%}g76t̤Rwso ;~g[zSH/?_ңVp E9fu}SYD2Ew~MBʼnS3RiJUMTRµҪ(8Ը9Hȧk؛bXY%K2pwĝa$#VrPsݸ'>O*)T~UU~FkmUVkqHz&4'ˆGg-)|`q~UUT}ݚ7!E<̎~ 2%4  y7XnLO0:HҪ>P3]I"Ďh$ )5_rc6lMY%& mփRf-1U• 92G3l6JL*V{]FkK)({)JGD Uj~2ŏ6Ѥ`W.ܬK.Α3*v7kKtͅ22<ɴ08 {Yg8wǂ9̉P2R4 1فt*$>=fdbexm첟ɢ MN % Hyc ͖Ɠ 9Fm#@h&zpRj,㜩GZ#h08M98e cͱ)v1+iXvi׎mI11٘WiR[cb-kQj6jڮk6 M?n(ʀ!A 2Zx*|05655@d 1xT/P#WL[`. iw e9vT!P M!jjy?v@U}:'||i"E ҾȆȈ()Ջ11!2 ;*v-nц6|T0(PKV g EJb"0-QJD²YOAXG2´Z @ yt 6]gh׳ bB2%f(g[$)7:f{<=1*bFg- (keHKjy`lR*RP] E9N; \EzS+hB']j&o00eC\inA|6XUyYY*Iƌ 0UgKݑ`X7laZĴ'"V[/ FoY7Kps3je%Xi^Ya?l$o^=<"xd5E6$GD*E|ۧ'~ITDTݍ#6tS\=wmrz˟D2FS~q[' F;Q=nFq!pJ\u=u߳&=D9΍9ޖqp3dUγsߟ33%42)GʒrMJ{c7R0MLbJ{wa[m)hO}?FF*I' ;LH% ^l5yAd$U>}QaLJ"0hvRUT$JDXml..@YԂWe8Հ*ŖVq3ͮ/%VȑaZT2u\Yɿ0vT|KEAvo)$`7L4s37ܩ_;$A(j;u!73sh١MѸatE a|vnB'󫠺ct!J~h+wOmP9G$2c25 .\ԨTcR+̀ƫȤ`EMp %^\e1di-7p*3Q֕J^%9J +_-JhmoqwD'aøHf>4eCQKHsB9KC|麸@"{4OLoYCQ}K< z샊]o ޠGZzoOSǖK QE_O2_h'R`3eC[3ۉɶI0|nʹ&Pޏ5HJOˋطY8lnT,  N'=,Q#P7)+!3+$nDyH6 .`R vLvnZ!*wLó)El&ʫTQP#δ!u ŅJQKrAmql(EH*I-6\g[ci%ї \ͅb fZCL ZBUmв1&@bTb}U{wm3idȱ-Cp416a<+MUx?t,}Ns .] 3AiG&D؀r$ " Hh K+Y0bƐHícVZ"!(L>2abQG7K&#mYKaGd0AapDDk|3g㪓x%AHz&(Z%)Ͻȥ)7hJPcHY4͖Q+뼯s.bn~=a7TtUtLYd C!opVH¡a)j9!Ce %UXOPth7* {8-!Hd(WQaXfIWz}qd1ybH(-> VSvDPY=,"5YdWֆыqLh%gE A je!L-BciF!w{i]BUp~$6n4΍3{wIHuԹ0(qƱ ?:[̭ܓ ݔ qFIaUJnjTXŒhU2 ,~ڵ@7 B<Q/ n8?R\D^$KieشQT!j)GUC+OL7&]Ǭ`FwSiύ#q7gm߅bl' 20ē` 0UpPbbAA"I,DKJ%D`$0'^5p8P‚~,E|TbyY's ρf:Bg-> su m=pBJq׋v۵lFJ]nTF@9P7xScISW gp~CԛQ\Cw c5(\fbp@Xeļ!8ۈJYVl8 ?>2mZtӓ<8֙F 0Nã9,9 D)I-ձiiiD393) I;$իJ ۔;mEJ^HIO7ʰW '1Y%.NFWc}QG7I#PֶZӅJ_3U=r9t{& ha@pmQ+E+؁n@~׉ n nz2s"P.LaZ 1X!IYXSM UEQYG:Jdh4>bk'M33'GGu1Qw-G)GXGG$%tuZ"b~yr s3b@bC}R0ٷ[a͇}[5 h8oFVJQ| `܋A,p1Ô:$Lhڏ;CKً2`U c(@khu"VͷU Z6aNwC~l̘L0?8)N!7QRD^ՕE<-1Xrdn9㪕c7&n5t.,}U E}&8 bXMmZl8^/1C[$蹹=T *~)pSO=As;v⛛lʦj0VCxR A:u=Mق'M'P+o$#+eO %=I)O>~i4;=#\Q&NTp†dT)LjR'ljiwy渆RDd[i"F5vne?\xo)'Yb6rɆՍ{X84ALp5:Q;h#ޝ'sTcnNK**G)dp6p,§4N:l3aZiŖyIlik2LIa2YW11md²zTg|=H#?e#  )`>uUdA?(i/hnQ62͓#َ k4kT*"ͰGtoS~aۅzrxd* R`l,KRF<3VEt̄֍00kxE@Rc[>#S2Ojv0p؛5IZ7Ol?}%ljET$3diQS47'' BQ7Ϟ76yǦ>:!gt;^MU OXL$e26&f5-IE$V>qN|kf1솉v"6qEqW`Z?8 ˢ{oNJmHFiOywNkMo?o,|T~?ja'Phd'KR!51P5 `! 萔H~@.tF%6F% |4AO#x+QHb &D.P7ws^V Pa}9# j(=TzHf&ıcǟk떼TLOV+se ɇ)shp&D'0d1mاsaDhnD"2%g2dO]m.b)#.WW89q54 L`100Iz$RI/%5{k%Y)I!JI P|T~*0LLdО ,OJi,3oYɲf,DmRH&H: *3ab`i>G E&&`4yZZ.%6TIS Q8y(9g~XRf5 ON1FJ0RRGTÆ3 (SM L)'`n 5, "dM"`R4J(?({ӇN[]R۞:]Koߓ\s-9yLD #l?AUUUk:K{7#YVl)Cd/$T~:zgS_=sj<x0ƈT}Bێzţ=F8dds?<~|=0v.;ަѮ72u'#}(!rON/п~MjS@2s :JG!8Qd*uw_g,)vNJ\1k]6X6 R<v-׷9$|fd5%5':@ڛf*o  }3pD<;*p1=bvۛ&mq6^ᵑ{GGЦN!W6fGL20r~JƒA I0r&,foH9#S4LEf,.Ds[g^~ T##'QzNG쓽 J'ݔ'8Qk(6 6@)"2 Z#݂UtǧTטb5*L3yӟn/,LFf!bM&DAɑff!bC{GbcP[˅al[w2^Be~s}B z`Pڲf؎B :S9a4C!sK9MxC ͋<)GO˜h-ş x#eč3َC<:nLMecnH#>ASNsT' 7s#3{ F'LETe]vm4f*u]R*iZ7iv3M4vk^YaFQ5 sf>f#@lJ>.cLuXDz P)d5k<1 5ǧ99rhܟq$7dSN=<&ߓ̒$$S,)x |0g0X2FqbR#(5Ŕb10(,#2ɁA,R d`Dē"a$Le)1 LҘT10R H A i, `(IK PѵvbYEfMrӾ"D,~ QKvaEb*\Mn%ֻؚɓXۤƮɱé ,&kL)utRiK$Sf䈮fh.$V阫7Ku˧N.ݗMLʜ5˷v鄳JSvmwԤ$'n뤹ԫ 5sU۷n̛,k\9]Br)[Lh]-4ɌJ5RhS5JXܖ0jb6e-A;6߆O=Bwtv &d~')0\MbUU*KX)!=(ׂne2XowSH >9o<8V0CPӓPQ##)x.e2髖e5JKIî 4TQbb)QLb21)L!JRX Ri2*j6Y)Nc&,V,\''#6Mrt"/ w@`dT-֙n)meɂ%$32=BCGU6r5ri)r9drbrqNm0Qvүί^㖞+$h,I<4U"Fc̏:G8N>FALft)[Uo2HnJ$Lo3)d,rO{Y}$)SxУaSÃյMz1be~.bqVOH^]K{NZ7kҤR1 c,,ϐ)N)]Q~AЙ,8IbwIJX0FAFhe|[6'8)f$#,%;O޶qݬG)Sl$d>3>'9rS0Gf2)OԜuajI@ey&~_z<2LD=ŶۃdYa3 U톓'|IQau e))Il[-jH%dY,ccVSl,?8z9bQ9f-^>1:%^ekm T>AŬ굍Cd;Q?r;q‘16f'D~8=إxRݵ)QɌW~|kie8u >Ͻs8}ԇ&j5TE-z݇#9qP`ևߎYIʷHoR@#!!  7O2!~HfSJJ~SӭkV'*`$==:w 6:>v<@Typ~ $~1 $1 4rX~Y"')JIJVK%(R|!2gOM '=H'ru-0ٛyӻK<^mGݍ8tob> p#{)9Ds,|TqwmǺ[L&zQ;bvpNO( #aĎ0!̸I.1z2|gUjY nY[e87=X7aa&廀I.7w9S Y8j`ᑦEXlI! 觱?Sa)=?Dv>ݣE'Ͼ/,œxbd tVY Rd,RR,=kV-c3C61`?pxI=Pqd`zr}r<}Ήuvii"K߇_օr*GhsS+NTf@֝ Fc1N͔g7#˓lOJ5-IǴIds|TrɶkVNZdf"dLaksø܆;pʥ)N SjM!`{  7qyt+o*l!T&rȍ&1(rß?޸êlCu I#*$w[Ab2!B}~pG&^_DKk9۽LLލZ4a QM#BTpx646~ùn7t-|ښ[ ڒA УrnFf7ffR2 Qp$ b$Spvl<هXۙx'Xm (ю(pǙm!<U\+ a;vw08$g6ɻwqr%XZֶVmѶڶ--iF@ ~?ZI(P~SN?ĞOr}BG䏂kfc2?t>b>?<ǷdI'̯Rѱ.ƌԜ922;K;'dکvƱt@P#(Ʉ2Gqzr\:Ys";5>1SNMU2g)MP~ Ýǧ{{U%MENm49n#PaYrQ\'#3OUU=X0Դj29c0\s8%UdeEc%()i NП X˓$Ȗ%TMݤ݊(=OPq# j''s03߹tsڇ $Iܦa'u^I)Dl 3b*TbHG)Mi;`WP=ztsß9f܎(I1$¾34W\s%ܝvs>놁k?k4 dW 89,[frԚ46~ϸ'}i,Y_!_HUג!prJTN94m&i͜e2&NeN9̓np$41񭶅0b1 V`1 b iNQc+>P;!؄菙$>qg$GHy?xG!7%=Z>hC9~x!3yOZIaH<"<kH?zJL^n<#&vi2O*9p\<X϶'줚M&Hr2p_`bX`XABJ !엫d&F~ )Yc'YoT ólGZ0eA4D<ҜlfrRuQLˊKilf3P0Ԟ^/)ᱛܜvhL{-`/g;.­bh4bW4$y#*5NQׇ{>~a9Q8>F '%54$Ke[ DK:HZ<*؃3l4È0{ ɨi4 nIrAOocc.G,СH=5̔C˪yPY(Ŋb*"D.Z@E?>a-rFcl43}JS*?o>{zRqJ98}}34?p֦hh)ihiI?o xȒBHXƺ7{Rb,"d^O;iNWw{er#O&c! KO jo #lS)׫X˾\'νnUotVJYI2,t0ORFT7f6S5.J~]IuJX$ƕN=`<1<)7 `bz!A聒]Ay-7 @( Y#y&n}"itONYrLI8SGl=XPmiUqJG44)PVXSjbXR=v8F U'&™46W_;fy^@ʪrD~y"/QÇU::XO9ޓ&g ;'>66bd=G">1& ,I\2`Oh,ȏ7K,:xt؟4p?cS!˕s/Mƭ[>o%11>+ҳ3=:4`aFvMl qɐS2L33$r:cpx6g`NU]~[' 8m +{Y g^1Ai)`Nh5bURJRPQ,dL%&IM#MEQO 1r Ʉ$oЌD0Ȫecw ܧUmot lS(h7uet,M<[m`r95c иUTє3 !8m ZLd7g:nGxj,$Yӏ!-DDSd^OKBq3>O{w:q >t W$(s2K;~$A'u)OI;hoBL l>R:>j_ ^:õ):PN;y[-~jիV]َ~U_*c0b v"v@qC0>=":Q!7/"'@ ܧ0~*p$cz~@kf_.cTqS#@[D?EJozҢ'j?G6m8qTp1M5- Q( ?;N1z~;2#FE)mXL3e7(4.FKD7}@M25;Ibj$yN2y)U;gaE eET RE 0cbO=[VhR~1aW,bS=Hy!݇v\A؛Gnƣ~bCg(}0{Ț#r0l18МqI?K,f;vmշ&tC.]vFY,4s Jbz0 *#;aV]:MdNjN6#ŖrL;{ sa`IlٚNlfsfDlnSIXz<^읬^%;|MufiL-(WTR*%eW1 ~g$?FDW>?vfME0`K)e+Mnս` iLVTmj?'G燐*SЍܶvM J&ݙJXdd04 )L 4T QILCA4&e21^/EkSOlB?LP!5M`FcZ(-U-Vy@c ?br#cQ~k?x!}ZN1,Rj",,kK}A7vAw|MZSD>!!;#䇳 "" O#^Bw$y'ڞxteO71`t><B{!=ɿ#:$㓎?G*?dnmߴ~!vR=֭{ov2ǃOy.=!L yB{VC!l.8"qf1)WŖ^7DabϜIؒoz HsCI$+yO͜eY$|~qy3JjG8O;hd|CA>l4m"V%Ye:3xOL{uG>\Z-'NTkM<ǐ2նD>?ʴBjիV6\AiSr vsCAsD8g!,Mc?tvڹpz^~ve7|-i:48C!en߼S@;!fmE7̽|G@L2~ӟfɓ&Lr+oǰ}3\9G'**ƣQs%))IjUfjKdY,c%K)YJfj-eYlc9e,*xቇ8šssH(h y%HC%(Q0T ru3L&F9&v,ʲPO]߽\^+<S#DןOx(4uc?p Mr{ɇVs*T8NSJS"LE*PN9EB~#)II,}+KiZŋS?U~Q8DyH xY99‡5͏?;PhrL#'>;؛l CKt9ON^斦Bf>!Y#ۍ7sֶ̲a"*~Jksa*sW?k۷M)l!CFHJ|Q?d*ji%Jo}Ut'@O""}(AWf[leUKxc N9mkmค'?Sݱ=da, Z[!`;K*d0p:]SpEt,)pӂc)K8w6nXQN:%7`V .e !(7w> _ߍ.)Ox7>Ђ *:%<,F,@c" leӼ}2>؏& M3jv<7qZlնRRR (K=,&,[PG@8fપJ*8IҢn5CL %&yZT8m-"œ.̦DYj2FhhL&# P)0Rb`0$I%Lffh040Q)T 0%*%J4&32h)2&FȊ2(И*hL#3BL IjI*i*i)MdN835mmk 5SfИv: &)/w,&8iƆeMMl^pV,|cZCa08w:mme"xo6*?{~$x?PnSRŖ,NPN1E> y&#ş~a 1ƛmI6$Q[W51JSG Ueۅ1h,,`X%eO,5&kBNa98#du6GkZSs̃2>hk ~sDDfͧqգ#9r $;T'~mrHn?_kBz!GHh;du{%2![E-u-*]٩,UW)1*tc䢙,N6 UbҚFi^͸d6R%L6IMc6W]\4Լvt۪Ͱ30C7vkߏ_jq~ӎn nRG 4n2FvG>Mm>'A{ڌ``n_E].\UtC~O >CvROp<ힴFT)JnG>s#Xsu1+"iVŒl[ˠxȍ?.? nތ%#*~uqenz3u>+G9})3! Xl,'sZ? i`rNS)41Ѝ"ML3Uz)&k%Jhz3z݇2#&G27pn nDCx 8FkӚFAE0LA4&C#H Nd!>`<-m RC@TPM&SF2~GqM>TqI% se*ܟq]j59%l yKuL,剝7gJƺes˲9vujÝoQep5&(xѨ;#CƼb13Ifiå_qMhER*ER*>i2e=?~4cw+?<7Ӗ?u"ezJtъb o\IMD5ȎdM 6Ɩج֫b*,-beVet)=?P(G@X[ Km[m#CI6'~w7Sۓ:rI#Φl II),RqG~IUG}ϩ8f>sF?t>Rm'b]͖22GK~\ɫCuyҷg'7 Rx N5iy?f yA7ӓ"9!i#nܓG~Wjիln+Q=όs43bFُ5-}1S5IjѠ{y$(t:~K?fK "-2P{Ћ:N0f)>TĔfS'/K‰VOum`>r7¢c$bsf;1ֲ+KwX%Rg&Y{!㙥%G9t94F6rsouUV=9xYOW=9>I>!ZU|i&C BB@(B ;GhMdiSh?-4՞BTb,ֶYmkBɬɒ*XزάgN$֋!:Q'Z&} e IԈb`wcYeʏҜc݌;Q0L~g 4?bBz<$$)MӞ!/7?mimvfnٙ`Li1BD郤qѠ' zONyl|x)D٣v#HBz!i!̐ߑNPxa<z?}=9$Ō'yGC*R9͞p>05(|#Pp3nJd> # .ye ÞwLcp1'>?T"qHsl ?y6j1~$HcbO4LL~aNrbeI uvԆ۱V4I&`np,|~qFGh8>hST㵲%? ff{k)O9㕭[͖l.kp`ډIgڿ~<Iqmlxq 4'IoQ~yO; F!3ؙkLOsNqi0ӫM''siMx7=m'! Jn'n$z$}vD$Myi 8$a\qVIja sddFLOdݞ56,ik` &Ni4̉OϟTi5OX40Wm\F~ú ÀQ YBy'"wBby%2jl933jT|QfK);l\\dSO\'?c q >z?LFde,gO&iC3C>I>rI mЃ?N)>>ОTPa6NA?4d_h>Pԟ5OO;5톹WZ+EIT' Qsv2 |`uFʖQe;p!g*NiɄ~aF ҎاsJfQm?X0%{7R+<9Md6,maʓv:ќ.&Ldɋ.JJJƺrRIhL[L"sb0Xo2LƸ_re*eev7tLc4{|'tO|w'3|r Ȝ9MNѸcT}yyR#pOY~hҚG<<ѿa9SQz1}6ۜa81~2VT$a/hPdhPqӼq&Gr#<܏~x0:y?l x;&>;?\54c@;cLصT߇LH8? KwAꟊ\? +R፲Ry7_9q$j;s ;#I)F<1#hdyjOǢ<@} z_a$e1.D9E5`ލ|ɰlhsPȕ՛s9#zi{1NI|u˕_On ­?'%A %5Ѵ,2mɽKjY9iYM:&9765 CRi>|<{Ƭ޺=qNT=b{T%'&!b0O6zzOc=6Gx/uhCu]UwTWv1ٻ7i&w ۈye-ZPrXŸ4ZkzDޏOp3gFY=Yayc-/ j{_NI68SښpX_@J(d4͎X9F+\r'eq؞d=uVˈ|6G'7 y>> Řq#־-ݝONYx=jծݯww˿M9ȳ=Ώ0k$xTy'٧vzo$||Ad'?㽱oNNێ?|NX>)O&/?uOz7G9$?R^io>8;t|4;bʞD8nO[vyK1zyS<#-$C3?@l,kz.rϞ8{“ϓ?>H )6ޏHwX̧=z#6֚LhA$9OnhN;iN'Y?yCz~!',){QdK7T+|ZdD9)Jz?|@Dp8)-ǘq ̓G6\ NxhhGN(hOD?7}=7COM<ܲ&(|'2ifjN}Y';;wsw=)ўm<NlGzy~m99ymwʘo|f9yzSexCoF<ٰ6o?PI-:y:YByތؔ}<#\bOZI#킓lSyǜYT!OO= = ?#8=ͧd~Ҋ}=d==AOzDB~(xS\n9OmvqOvNQ8$ڂw= Y\_D'%K2?*}M+(O#-8\&|NcRq#4ù3Drcz3t#=Ǐ:ʏP\x3 &u 8,NnkyQO7{1ʟ?|Vyg\xSΞy>奄-Sort#ɟtSQԎkn{DGpv}B}b|#O5Bz29Q͜凊x?yc:c 97><Oc0u)XL,~~*fQqYkW !!  $p3۷$XWSD5MMRSVUYev6 S':v4s?8G6?,}1G DdxA8zC}8GܧNNH~S?4~L #FbO9C:v;P Rq9؟(8GH$I9iV\=SŲ~W9ObOyOGOޥ: Uz}Q'&Kav\Rilǩ8c$-=#?j7%) pr W핆m 1byxv咬!O9Nnf03o''|׈CyԞ#oǬ4Pu6\t#|m:g8RX^PNT<8>6v5,- vǓbǽpDsGq.mEy8''Ћ:yُwx;3l7Gu>ݎE'x=) g`3?'lSȔ|9ӓ6gJxə܏}3N9:S=ϟw䝼ȟr||y8=\۞Y?r{OAg8ݟyCO|yXCc<>tȳ:9ٝTS~9w^u3fyӮ?b}YOdz3u*~4B>ƹUfy1;*TګzzJϽ~ZzԽ7>`?Lr&IRҞ>DzJDrG:N9Cu?93iV$IʞOjo]G>}ikOp~G9#⢺ϐe/xi#\~] =Yha5l &3wse6Ny2$B=sƗ*[,ۏ`& I4Dw=r}a#Hxs>#%2k> ʜe>x'Óc\jPdX:?eǡ1E_^f@il4F[#S-S♭1Wl04N;Æ3gXld.L /ҫsR@qa%<+@~`oϭFpJpw7sre6 ~T9| +ee|l'^g*ym݉6CsSyv؇r _Xy+.L0eV}IJ8~~; CgYv{oG3|1?ࣶȃ#[ظs41~<)iOD?А#~W4HGd?->B}31?Иyћ1DOfztHȩ`l!J|< >ffJRddL JJRRc=m IX;aG)>ea^>6`TثvpS&&NR #%Q?UFju դqs DETQUTx7Lw O:qP:tm><y;:xȇj!އM'lގ o)ǯ;_ ra:X{ᬵYK _1zڵ?d41o##VTLEV@o&p2mϬ'uc= K4za5䯠sg5!S4`)'/y^zEP_kU.zM/#UUw`=0[0_ ,p+%SGLw\c09o?v3;\է'D<<K:Y櫫AXZ֭*վR]roZ@~,tPa͚g?Ixd%:DѰ{xz6dۉav_Ewwy^JsIbJ'K=2Y'W:g>LN:nlq5mڠ&J)hҸ1 '4=tr|14h~|OϔUC۴XM 88`rN*m)3͙4Ui*}0}ޠcD)lMsCg.C-9 %^2ז xK/ xk^f@@=!ɯ) Ye 40t;.`_O;S@ :I)-#RArF- :;~|^N(D8wKsfbiWMw\jKuax!gI={_J㘑g(cIFC{LlךscrsXڟ7 C;!yi<t"t.w+A]׮pVh >S<2v)7G=ތ0ܜQqOo+d+9x1'j DU9DZS?{g^\Y魲⇎{%пF% 0"/s71Bz# u4vL#zT|HO\- h'("z((W$L;ͺT֒YʞՉcbM|'D8}ۥ:Iچd͜/2wqǠnU^mY= ہ Y͍͆0Esg:;c9ddž#6;Ns'ޙ)6fgD6'N\:y6O lEʛa`w"LM4fFS cpL{smq$bu(Z<npFYDx ' ĥ8aX6"/4ږ]PlqCr"'2[pV])C e]]ʌ==8Nx}}?twnk"BtT5Uƍn ( KMr&ܝ7-0͓5mù,[^ӓ0zNXsD#tqcO3,YD(rȉh yϑF#D)=EL# AALc_g:8z13=wo᏿õ*ȏi&6&S^(s>1%Cf& Mt讫] sVtў nGN? r;'({qފ9LJxvsg,7vdI'DN2;a910<0Mѐmw*۝J}7$(cJi'HEx6 \d}Ry!ɜ|?hANUN6O sq6M_u߱y ;a'|n ӝa̜z@wϚ# о*z%70xs5ȦM٫4zp!˙&扥7]Xn"".3Bn+hE&l 0pB!PC qzck&&jxYcR<4 c"8s]02y`|%B֔Um°R-N9SO[Uow̙)h!qύ#Óv891 5x13o^+sd!ӑ CNK<)$r&ď st 9q^itɐ M9vg p8G/8XS\vӤFnI6MЧwMp 5UY\1\7CS#v7M&֛x; CxQQrҩ燃)G>G%$ؓ6 t2;&0*rd5rCvNtܛ9y&95ư7Qz[m'BM6›C;38ɐӥ˹t٣:qZknQ8Ð08x;qG6<3vh;7a9__>1,<`)%6G2h6!s'&7cHR3!4Ҕc0tsFu1369M* a,{イKfc<T==tG  MFt4JiNI䲮1pM1$g#9i'0FFFFGt&rnIMܥAX4Ii4)+p7A37rkNTYBMܓe)(lJ0m 7#5Jb1lFLH*[*̎9FC"hNd7bh:qdݍ9QO_- ?*v%Ze[- ͺPn0FfݍdALdfʎ2攴Ֆ1b2V86z3 ;xU`ya۟##=clbނydQlV`BCh4}o͑Ŝ<*JscGwRr&>)5ߥ^TR u6N;Y|8('DMr9͝R80 IIɎks݇[g.)[zDN8iv X''$ӄ/V$}7dqFLOdxpeͣ5o^{V0YωNhf4S;Qp)g%%ޯؐ [HQ64 A0H:h Bf G{8_;h:μ;Ƌ -ZYu5٩ G~'wcC+6u^.9&%;PO؍2,d5eC$nO]. }GW;ttgzQ\tC^aЧ|H)4t5as2w𰜐m`f#;aQpHvSew~>q;{)ZXwj%;= pJ6m+]CJ4e$dgZEYguO`N(: ;u:#>^"%a} [5$NdrNh#rw1W9d͚aUk19E⿼kkwy}jd9?y(hzOsuZ}屜GZ?I~<&g LiwĞaO9SxG&xCxyhH;>)5tGDΞ,~'8xLCr~ zׯ}1/y/&cEHѓeA= ><;!!ց НϜY_gР2jCY51,gAt8ƄKP/x ۆzT%v'}"Yʦw#C-%vz|rCuz_$$Y}ҫFY0Ɋ(rŪ9*㕝+WϱW Wz[y,۲VYenv#9L=)X;O::Ԏq}ra~48Q)O*~$bOOK1jpL}?5I~m}֝_mto'O9w!:㪎j44yR>qi#~uO U&:Zv+Vz<9DCyH J7!W=G&K%܍fTA,@&;M>z,'d&z<8fB GJC"zߎg<Tz pu*˦Ju)zj9Os >ܻYnOVM TdJuZ6enqY1VBʲʇ~̿dlu55UA^kSGMk"PtmxCقY:/94R]i鿚Ib@|G>|ϜOSzw(Nuӛ|Al)GIMYipu`yVygzvasf4b-1m@܈݇\] b45G:y7Ҕl %A7kl%Im1DIHh>9 !0"X;a >{w{sБ:sЋ&d1~ +-mZ(E Tnl>xx=?D\~Uaە5;-Ι0 a bo^7n<PQ)߳ i-,\p96:$gUoA 5cloxܱ\nsR!03Z 99%8CC! Sy,B'&='+c63ϝ:I wrO =RrO`=} k&kvLb[iLVIW.]߰6I!4ijnWԺ\ZDm6&57^omGuzF )gLgvJ)"Hɵ$$l&e,h@ƨ*")(cEIEh-&131H)b"Sk{ꪪ+^ hjD;Ѿ]|>b}bv__n !dHI@I-i,O@֎X'2vVtemKd$54)`9WJCEMXom6|:u*^;~|ƣpS\v*A1 Sr5@V leEz6!'Ht m՝(!F(5:YK,N}3ٽqS i b,&;tgnߞ^>0iMVhPjFyŕOxsLc(P՘#tI SL Qi=D' DT?X~P{!Gw p#ٕYQeufz:؏P;Q-7m:D"~>A>TLfO~LIIDșA3&DdN HT=|GhT<oN뇃g8QOo:ӯ;0|Xu)SrY^I7NG(䉺Mapt?lݨN~$7m1$LR8(٤TW> cPvtSNZ8Ec@ 9pcIn۵TF9;#)B^& GIIX& 9 000`d(ԔJJ&eIJR*0P00)M%,4&fJS#R0R'F%&`)†  CIȢRДDF ddQ@Q)J(i%*%20bRQFd&"R)Hr>>^݅ ~`nٝ$惒ŽTFe*Yܼm\(XQJQE)E)E)E))JRQJRQJRQJRC޹vWǡwoKO36CG|kl[/lTzQRH) ȤI6mU)LRTMLU1 3"hD6l̤ %#%6dV8!:m4Oʶ>1G,DSqMN fIM<=já؞Mcb>fqDv"8ܣҍ'jNdn4Ż)2k1fMQkRxeJ1&?p R9F?P;K8G%9A[lF lgSUHcKo,FɃ '>U4?DžN2y8?LϥǵxM7nnqq-YvgߏX|Ҕ~}y@䆇NL)[`x!LN\~k0VRZz1 vjm:8zRG1BIH+s'kʹ>Gw"c7T^_HI&h/)M^}UIU)fQ)  1%S2RRRd`dRQd h2&r2SD 0 ‚ t SkL,)JR S"FF `ȖfQQJg1#IL(JUͺSm2Rf z@4P1RB&FE)dQe4]Jm6ͽ]R*t<$ IfС@ @F1IGp8 pAۦ`*;5tmj@ْF(($ WQjb1wɋ#\&U[ՋX\a9N$N4O>$8`}ow߆8Vm[ѓ<O?fMsϞ}FfgA'$R<➼0VlpEՠaqJ'Å)'I(.8q#Df)|ML²+$1dfRH&ĔlXjB ѵFq~2=Rs'u؟d /*8<cxу|IJ,bTLRie U :Ҋzq6st995=4w͝Y+U^? RjAڇOls# ބcvyH'2N>hO&O.Rp; KTclc"[Ep1& d܇%e,c `67Ot2aWN7~&6FH:gWc6˿噶 6՘j`VR!KmifF2Z!3n1OI@%%)JS=@'92q'9H3c4ɂ}*?ºb*g&,є8=ۆfgR se8Wsm)_W+l3~Yj­<cwRy'{6$wɟ4M'5nokL Cy"(zN~_jۤhf%- )z} sR%r *BƁK$8G  t4IM6S.{<Οi$~wUoԚsL660L .Ea /gv Z*PZ~HPOt_ N9? ? 3K?y &6t>֜9ރT0,,e-JXbq^'ct|䍗$ Ƌ,in8O$&τ5jq=YHjBTUS%aXhi4YU`R+ ڟzUy㻺4kV=Ǻ#w8MvR★gio ۏ=CO> z.m7u~@#F HTVb1`4֒A2A9 y 5ܽަaMa&^I @okCвNdt-jُF;bz1d۝p#ʟSOv X>0R(otO%<{p0a-)[Qwa"qާɖ1h!yLaSQC+7ƺ=}~TĖc 3JG-o>acl0T)l17Fe6mcv2))dI8?=q?y!#$Qp '0M,dᓒ;brG&%+<6!@@{ iV/ \b.Ltg-a lF[[ b ۅsgO9ώ@dwU:b#󽿓)# 1=`pndrd2t&3ԝp@N}a4gp|}QbR$GmNqKRpC)- iK[K\S-Ѐ̐(HR;ヷe9c>A,h=~*j٧a"H֓(4`q,IمS'?|4LdYZU7$fy'J_/ J'St6yv=NC67SQ s_&vh?q>nIj##);FANI;Z{#gy1G#O<<#C*COdwy4}>q\ZQʈtn#L,@#2շ|? Q7ߩ&o+fx`p#5f6Wr:ڵGKi:Q#HaDl#BOz=s2>4 *+ &9s{G3fɥ=$ב6VQm&ddf[9!*t#\~L; Lqr-9#X߇]wɚsFJO>R2{{m Lā?d'#؟p=q~8ALPTsO2N)Ċߕ鏦whןh;Ȏ4wp{]H`ǒ>b66Lc9Q4t?iiwueW?铟Z}t܌:(Qul7j4@ jcA1г@,w`pHH.4eDQQ D!H $$IbF#]f&JdyJTf"J 8JK$A3"tdWmRЀ1!wل0ASH%% IBSeHEq0ccl e Pg"0,C d Lړ`EBaj,"!Kp)<)b1%4ac(BX444,Dmcv " )DiQ"06`DJ2%Xcc@"% 1hvbX 1)$I: ,P4Q!JR0vl1`) !WB0a!`:GV`i\(!'MӏR'uOrbq\_y3n=Q؆GhOs).9Qm#SN/Mۀ؛se lNd6O)D)kNP8q"PF,orI'n>I60iZq"Ӎ61 iA4M4tcM884mƛc41itCM6c$%iR2,HH4!cN1CAt1YfYYݺ322̲2nsLcM1 1xyRTx@C8:c )4*0j=¿OQlWZ[+kG@?RtqG*ا@۱vhN:1cu(XaCM,R"(y8[a#ǀ{X6e5]O<^P5L'@kR|3,33EE2 3,1e,09xOwzrM5#T~X?,llTQsf#Q<4<yUfMCˏB99IMϓx'0da:MC#?3qf[dqɄT# +ģܱ֫7l`oi7I7<O2$ۄsl-216*}xA-|D qn%5b⭸S㍔ F*U*KUMDܓ]5\7̵r5y$Ԟ54H_o"[?a},6@)ߕS:s6bv$n 9S8ѝd G7NI9voBTc1"i]&FiGM1g<~b=] H4:TBpɤ6Nƒ$Rl%batL+i!M{OHHx&0N * r 1O6*a7!/۔@<{DHftCó7,ZѲUBOLaaOJM$6J1X>T8X9DV-6JIfDJKj%,IIt9d6+\ dOYWP=8d߷HxÜOߐ:XXA܁H?t:X?a$' HL*sb)2D|ŌbhHY`IdfJLHlh!P ,I%5bI$危 sUSa=kfӮ8i $y ɰyQUdzHL*X a}u%o[ 34X&iDH"A1&L5{.te~wj"_s皜-\3V~SxCr^ϗؔ]tvEL%SnRId,t#uݛWwTGakƱ鸍m7 UV+2L0Fr,HũKl2jo'#nO'ɍݶyhW7ۉbmgxwq~Dëpg靂II2 Fz=3l2Ŗri6`DҁHP1V, عY112}w|&oUW|3#w;jRKkmp=X?~A0}2I5X')(ޘCB11XEL#S2U6H))>ș #1<C2P~~397"S 0!FP঍FnlȢb blءIJ ID*0['TP qXL )JS))iMz)@jD,̖™aҢI5Db؊B6ԕ&ThFIwIѹh`L0唤O "0Jh.WH慈ӡ/F>:Пt>1QQioQvz%>2I G P$݋$4 ()@30TRC!PGMic柲eG>}#fDtӂwD?w 9qN\Rw܇?(1# DTHDHGN߭#vGWpR TJj[_?Ltǵ `qi8>j10ctYdWW1δZ~ Zoq!7TN-}i4qvL!DƍZ2k-lmD-mZmiVjUDUm)`-U+Z­n9'W\\n:;swwu6*BʶҬ[F~ᅸb'4nOXsIdO?:YݧN_a2?/[_unycħE)z~M=)?1LSTZSd0i6G7$$}]#TcG4hZaIQL#&a? '̧ipbӅ|fE[ZaY|O]>OzQX?L/=:n^X&oa- $sѻ:Dt0d3)tF]9]JP ڬT $&t7]&cQLHl:u\7!=Pui][~F:!T Ն3ӷ&NcwIxxPh7Ļٌs@KKѲYal!E9~Jڃ7>?pշ[u7L1qmK@q #Z50A"0UN7)ф=IQ,$G*#%HG&$8f^-==1'6[o<:\]g^C14\opȔ9u[fe*%8 И9dYQ._%svY޸#(?@9})YwNXN-r=~qM̫wv-P>P|qV Y_i%16(fyMW4 *_L{A/f2m٩b:&Z?X0emn'XRTG7f5gqr#,i:"'6!=a$F9wҔVGl靕h8&b5l)c7\.tRsHųl$s?2`.S篪ŗ8sUwHh{L<ޜLӌƮ鼁>mΆ#᧰9>xE=$1nO7 E_k,}a"I&c9ܥ5*[v*jYq9tQMpp*mZ茱 &󜡎79alsr4`bvb]drmx~$228c+1'!#b@EIV%V )S4?>tvSOݵo,xӟ}CL٤5cX5cdOiL+:]b!'@sbssZa2:s=3yb|$hoM*;@"{Aa?R()' E('HdS#@0EBd3AbYd&b a#J)dJT*"k,&:}S$&䧒F.L揌rr:Vꇊ>q$7ҍ iO$g0ͱ5_J0^:D'dPphTǒ1qqt dwͩQ䏷)G}>ΘcnncseSOvs\^bTSa9Y1-VYV`TdO6;MY9Voi8&mbFݺA\֭g#'@wŎH'Ĉ$AFmҕee_O2,露Y! .|I!޽ Ij6yņpUy3O|Iv)R}>( HCݾ[F+AJne9XVcrKDฤ>emJ~Կ޶gw$> = ' “##vJ7nUaOUm~":%l(|@|C iuL0B2)uJSE6St#8pNvPaږ)[Y0F:*^PH(C/*we0gNB(v1&+',+S{z[C8ݶL.DjYe*,*5'"8F `RYhL2 aƸ\!PI$.P8X@ MJ44hXh:HXڢZL&JV 2L3kFFLZJɡ&f#磲#؟`|NN;#=CX6̈"o8n}Щ`5D#N͍$vbv8~@?\zOtʦṳen"ϼ!@, tT}ًQ^y%_WFE 3Lڌvi'G󞀥=H<Tz<xrgI'E6޽ҕN Oh{8CpnD!{0}<&=Ìxy QԞ'ȰHMX-%ͤ6@dci&5dd+#6fk%_~}O}4Rbⓐc7IK&VtFfFQf Y-z?ZY'42`v3:#,WE'Qi~4%S0Laq}{4jq.8DFnR+<<֧ƢSa$d̓p<DzQ*|1ٴ^>;I 1(78gۅ=}^y&L4c#ֈSb٤Ш, R/ cpFCLSIÂ*&I4v:13kԇ`1ь;G,O.<&go8#-mȌ)A=pWA mC+`;5J?~I4x9!<$I!ZE5"-%ւW'~qpѶ<yp١ONoq jyUկba^ւ"il ^jUZΥ9bW3e*8't1TWM{Ren۰6ozՓ/Sn#ʒ_3S7+V!!&B*xZ vV&I,@ Zla=U%=W]@p sA3e-d»{q?S\ޓ sbaq)**"77ɍ#MCA]8T@NĒAWO:IiX#ZH I6 nrSN9Vw<,R2S&l-52n#VR?vCpCe/M#al1c(Dn `" JX[_\yS L&8&&N[o#FjkQ4&AVoΜJsf=x/ 3h0AyGB{a;#=;Mu{oyYQ0Dت +'˼Z c5]T.@~7 0}0vr2` ,zQߜUQI߉STG'joIU5y4b Nyky}\/LhSf)3e1I j{!|3c 5Y2407oټo g(|ԶRܣ \ jj|P9+=RXSv Y4i*De)a2~_?ş!g;_bmfx1ѝX)N4HYgFhw}/bABibRU-HhN'D%f'@'I BI9C~Y<)˜UwH)=dI!A0~!x+#6VFUeѥhiegVKm^+`~DV֋D14ԒD-5MK3M&&J% 4T?Py1RΖ~<~^m/I;1Q S=8u(f"~P|^0H~6!ʦo*dL=>yڛK,6n0.G8o=>qJ9Vjd a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "serveropcodes.h"
+
+const static ToServerCommandHandler null_command_handler = { "TOSERVER_NULL", TOSERVER_STATE_ALL, &Server::handleCommand_Null };
+
+const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] =
+{
+ null_command_handler, // 0x00
+ null_command_handler, // 0x01
+ null_command_handler, // 0x02
+ null_command_handler, // 0x03
+ null_command_handler, // 0x04
+ null_command_handler, // 0x05
+ null_command_handler, // 0x06
+ null_command_handler, // 0x07
+ null_command_handler, // 0x08
+ null_command_handler, // 0x09
+ null_command_handler, // 0x0a
+ null_command_handler, // 0x0b
+ null_command_handler, // 0x0c
+ null_command_handler, // 0x0d
+ null_command_handler, // 0x0e
+ null_command_handler, // 0x0f
+ { "TOSERVER_INIT", TOSERVER_STATE_NOT_CONNECTED, &Server::handleCommand_Init }, // 0x10
+ { "TOSERVER_INIT2", TOSERVER_STATE_NOT_CONNECTED, &Server::handleCommand_Init2 }, // 0x11
+ null_command_handler, // 0x12
+ null_command_handler, // 0x13
+ null_command_handler, // 0x14
+ null_command_handler, // 0x15
+ null_command_handler, // 0x16
+ null_command_handler, // 0x17
+ null_command_handler, // 0x18
+ null_command_handler, // 0x19
+ null_command_handler, // 0x1a
+ null_command_handler, // 0x1b
+ null_command_handler, // 0x1c
+ null_command_handler, // 0x1d
+ null_command_handler, // 0x1e
+ null_command_handler, // 0x1f
+ null_command_handler, // 0x20
+ null_command_handler, // 0x21
+ null_command_handler, // 0x22
+ { "TOSERVER_PLAYERPOS", TOSERVER_STATE_INGAME, &Server::handleCommand_PlayerPos }, // 0x23
+ { "TOSERVER_GOTBLOCKS", TOSERVER_STATE_STARTUP, &Server::handleCommand_GotBlocks }, // 0x24
+ { "TOSERVER_DELETEDBLOCKS", TOSERVER_STATE_INGAME, &Server::handleCommand_DeletedBlocks }, // 0x25
+ null_command_handler, // 0x26
+ { "TOSERVER_CLICK_OBJECT", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x27
+ { "TOSERVER_GROUND_ACTION", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x28
+ { "TOSERVER_RELEASE", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x29
+ null_command_handler, // 0x2a
+ null_command_handler, // 0x2b
+ null_command_handler, // 0x2c
+ null_command_handler, // 0x2d
+ null_command_handler, // 0x2e
+ null_command_handler, // 0x2f
+ { "TOSERVER_SIGNTEXT", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x30
+ { "TOSERVER_INVENTORY_ACTION", TOSERVER_STATE_INGAME, &Server::handleCommand_InventoryAction }, // 0x31
+ { "TOSERVER_CHAT_MESSAGE", TOSERVER_STATE_INGAME, &Server::handleCommand_ChatMessage }, // 0x32
+ { "TOSERVER_SIGNNODETEXT", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x33
+ { "TOSERVER_CLICK_ACTIVEOBJECT", TOSERVER_STATE_INGAME, &Server::handleCommand_Deprecated }, // 0x34
+ { "TOSERVER_DAMAGE", TOSERVER_STATE_INGAME, &Server::handleCommand_Damage }, // 0x35
+ { "TOSERVER_PASSWORD", TOSERVER_STATE_INGAME, &Server::handleCommand_Password }, // 0x36
+ { "TOSERVER_PLAYERITEM", TOSERVER_STATE_INGAME, &Server::handleCommand_PlayerItem }, // 0x37
+ { "TOSERVER_RESPAWN", TOSERVER_STATE_INGAME, &Server::handleCommand_Respawn }, // 0x38
+ { "TOSERVER_INTERACT", TOSERVER_STATE_INGAME, &Server::handleCommand_Interact }, // 0x39
+ { "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a
+ { "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b
+ { "TOSERVER_INVENTORY_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_InventoryFields }, // 0x3c
+ null_command_handler, // 0x3d
+ null_command_handler, // 0x3e
+ null_command_handler, // 0x3f
+ { "TOSERVER_REQUEST_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40
+ { "TOSERVER_RECEIVED_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_ReceivedMedia }, // 0x41
+ { "TOSERVER_BREATH", TOSERVER_STATE_INGAME, &Server::handleCommand_Breath }, // 0x42
+ { "TOSERVER_CLIENT_READY", TOSERVER_STATE_STARTUP, &Server::handleCommand_ClientReady }, // 0x43
+};
diff --git a/src/network/serveropcodes.h b/src/network/serveropcodes.h
new file mode 100644
index 000000000..77f39e09a
--- /dev/null
+++ b/src/network/serveropcodes.h
@@ -0,0 +1,43 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2015 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef SERVEROPCODES_HEADER
+#define SERVEROPCODES_HEADER
+
+#include "server.h"
+#include "networkprotocol.h"
+#include "toserverpacket.h"
+
+enum ToServerConnectionState {
+ TOSERVER_STATE_NOT_CONNECTED,
+ TOSERVER_STATE_STARTUP,
+ TOSERVER_STATE_INGAME,
+ TOSERVER_STATE_ALL,
+};
+struct ToServerCommandHandler
+{
+ const std::string name;
+ ToServerConnectionState state;
+ void (Server::*handler)(ToServerPacket* pkt);
+};
+
+extern const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES];
+
+#endif
diff --git a/src/network/toclientpacket.cpp b/src/network/toclientpacket.cpp
new file mode 100644
index 000000000..b51da48cf
--- /dev/null
+++ b/src/network/toclientpacket.cpp
@@ -0,0 +1,28 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2015 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "toclientpacket.h"
+#include "util/serialize.h"
+
+ToClientPacket::ToClientPacket(u8 *data, u32 datasize, u16 peer_id):
+NetworkPacket(data, datasize, peer_id)
+{
+ m_command = (ToClientCommand)readU16(&data[0]);
+}
diff --git a/src/network/toclientpacket.h b/src/network/toclientpacket.h
new file mode 100644
index 000000000..b926514fb
--- /dev/null
+++ b/src/network/toclientpacket.h
@@ -0,0 +1,38 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2015 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef TOCLIENTPACKET_HEADER
+#define TOCLIENTPACKET_HEADER
+
+#include "util/numeric.h"
+#include "networkprotocol.h"
+#include "networkpacket.h"
+
+class ToClientPacket: public NetworkPacket
+{
+public:
+ ToClientPacket(u8 *data, u32 datasize, u16 peer_id);
+ ToClientCommand getCommand() { return m_command; }
+
+private:
+ ToClientCommand m_command;
+};
+
+#endif
diff --git a/src/network/toserverpacket.cpp b/src/network/toserverpacket.cpp
new file mode 100644
index 000000000..7b4968679
--- /dev/null
+++ b/src/network/toserverpacket.cpp
@@ -0,0 +1,28 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2015 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "toserverpacket.h"
+#include "util/serialize.h"
+
+ToServerPacket::ToServerPacket(u8 *data, u32 datasize, u16 peer_id):
+NetworkPacket(data, datasize, peer_id)
+{
+ m_command = (ToServerCommand)readU16(&data[0]);
+}
diff --git a/src/network/toserverpacket.h b/src/network/toserverpacket.h
new file mode 100644
index 000000000..eb8470b07
--- /dev/null
+++ b/src/network/toserverpacket.h
@@ -0,0 +1,38 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2015 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef TOSERVERPACKET_HEADER
+#define TOSERVERPACKET_HEADER
+
+#include "util/numeric.h"
+#include "networkprotocol.h"
+#include "networkpacket.h"
+
+class ToServerPacket: public NetworkPacket
+{
+public:
+ ToServerPacket(u8 *data, u32 datasize, u16 peer_id);
+ ToServerCommand getCommand() { return m_command; }
+
+private:
+ ToServerCommand m_command;
+};
+
+#endif
diff --git a/src/server.cpp b/src/server.cpp
index 399c41b70..826350505 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -21,7 +21,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <iostream>
#include <queue>
#include <algorithm>
-#include "clientserver.h"
+#include "network/networkprotocol.h"
+#include "network/serveropcodes.h"
#include "ban.h"
#include "environment.h"
#include "map.h"
@@ -1149,12 +1150,11 @@ void Server::Receive()
SharedBuffer<u8> data;
u16 peer_id;
u32 datasize;
- try{
+ try {
datasize = m_con.Receive(peer_id,data);
ProcessData(*data, datasize, peer_id);
}
- catch(con::InvalidIncomingDataException &e)
- {
+ catch(con::InvalidIncomingDataException &e) {
infostream<<"Server::Receive(): "
"InvalidIncomingDataException: what()="
<<e.what()<<std::endl;
@@ -1164,14 +1164,12 @@ void Server::Receive()
"SerializationError: what()="
<<e.what()<<std::endl;
}
- catch(ClientStateError &e)
- {
+ catch(ClientStateError &e) {
errorstream << "ProcessData: peer=" << peer_id << e.what() << std::endl;
DenyAccess(peer_id, L"Your client sent something server didn't expect."
L"Try reconnecting or updating your client");
}
- catch(con::PeerNotFoundException &e)
- {
+ catch(con::PeerNotFoundException &e) {
// Do nothing
}
}
@@ -1197,9 +1195,8 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
static_cast<RemotePlayer*>(m_env->getPlayer(playername.c_str()));
// If failed, cancel
- if((playersao == NULL) || (player == NULL))
- {
- if(player && player->peer_id != 0){
+ if((playersao == NULL) || (player == NULL)) {
+ if(player && player->peer_id != 0) {
errorstream<<"Server: "<<playername<<": Failed to emerge player"
<<" (player allocated to an another client)"<<std::endl;
DenyAccess(peer_id, L"Another client is connected with this "
@@ -1240,8 +1237,7 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
SendDeathscreen(peer_id, false, v3f(0,0,0));
// Note things in chat if not in simple singleplayer mode
- if(!m_simple_singleplayer_mode)
- {
+ if(!m_simple_singleplayer_mode) {
// Send information about server to player in chat
SendChatMessage(peer_id, getStatusString());
@@ -1271,8 +1267,7 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
actionstream<<player->getName() <<" joins game. List of players: ";
for (std::vector<std::string>::iterator i = names.begin();
- i != names.end(); i++)
- {
+ i != names.end(); i++) {
actionstream << *i << " ";
}
@@ -1281,1467 +1276,1605 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
return playersao;
}
-void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id)
+void Server::handleCommand_Deprecated(ToServerPacket* pkt)
{
- DSTACK(__FUNCTION_NAME);
- // Environment is locked first.
- JMutexAutoLock envlock(m_env_mutex);
+ infostream << "Server: " << toServerCommandTable[pkt->getCommand()].name
+ << " not supported anymore" << std::endl;
+}
- ScopeProfiler sp(g_profiler, "Server::ProcessData");
+void Server::handleCommand_Init(ToServerPacket* pkt)
+{
+ // [0] u16 TOSERVER_INIT
+ // [2] u8 SER_FMT_VER_HIGHEST_READ
+ // [3] u8[20] player_name
+ // [23] u8[28] password <--- can be sent without this, from old versions
+
+ if(pkt->getSize() < 1+PLAYERNAME_SIZE)
+ return;
+
+ RemoteClient* client = getClient(pkt->getPeerId(), CS_Created);
std::string addr_s;
- try{
- Address address = getPeerAddress(peer_id);
+ try {
+ Address address = getPeerAddress(pkt->getPeerId());
addr_s = address.serializeString();
-
- // drop player if is ip is banned
- if(m_banmanager->isIpBanned(addr_s)){
- std::string ban_name = m_banmanager->getBanName(addr_s);
- infostream<<"Server: A banned client tried to connect from "
- <<addr_s<<"; banned name was "
- <<ban_name<<std::endl;
- // This actually doesn't seem to transfer to the client
- DenyAccess(peer_id, L"Your ip is banned. Banned name was "
- +narrow_to_wide(ban_name));
- return;
- }
}
- catch(con::PeerNotFoundException &e)
- {
+ catch (con::PeerNotFoundException &e) {
/*
* no peer for this packet found
* most common reason is peer timeout, e.g. peer didn't
* respond for some time, your server was overloaded or
* things like that.
*/
- infostream<<"Server::ProcessData(): Cancelling: peer "
- <<peer_id<<" not found"<<std::endl;
+ infostream << "Server::ProcessData(): Cancelling: peer "
+ << pkt->getPeerId() << " not found" << std::endl;
return;
}
- try
- {
-
- if(datasize < 2)
+ // If net_proto_version is set, this client has already been handled
+ if(client->getState() > CS_Created) {
+ verbosestream << "Server: Ignoring multiple TOSERVER_INITs from "
+ << addr_s << " (peer_id=" << pkt->getPeerId() << ")" << std::endl;
return;
+ }
- ToServerCommand command = (ToServerCommand)readU16(&data[0]);
-
- if(command == TOSERVER_INIT)
- {
- // [0] u16 TOSERVER_INIT
- // [2] u8 SER_FMT_VER_HIGHEST_READ
- // [3] u8[20] player_name
- // [23] u8[28] password <--- can be sent without this, from old versions
+ verbosestream << "Server: Got TOSERVER_INIT from " << addr_s << " (peer_id="
+ << pkt->getPeerId() << ")" << std::endl;
- if(datasize < 2+1+PLAYERNAME_SIZE)
- return;
+ // Do not allow multiple players in simple singleplayer mode.
+ // This isn't a perfect way to do it, but will suffice for now
+ if(m_simple_singleplayer_mode && m_clients.getClientIDs().size() > 1){
+ infostream << "Server: Not allowing another client (" << addr_s
+ << ") to connect in simple singleplayer mode" << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Running in simple singleplayer mode.");
+ return;
+ }
- RemoteClient* client = getClient(peer_id, CS_Created);
+ // First byte after command is maximum supported
+ // serialization version
+ u8 client_max;
+
+ *pkt >> client_max;
+
+ u8 our_max = SER_FMT_VER_HIGHEST_READ;
+ // Use the highest version supported by both
+ int deployed = std::min(client_max, our_max);
+ // If it's lower than the lowest supported, give up.
+ if(deployed < SER_FMT_CLIENT_VER_LOWEST)
+ deployed = SER_FMT_VER_INVALID;
+
+ if(deployed == SER_FMT_VER_INVALID) {
+ actionstream << "Server: A mismatched client tried to connect from "
+ << addr_s << std::endl;
+ infostream<<"Server: Cannot negotiate serialization version with "
+ << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), std::wstring(
+ L"Your client's version is not supported.\n"
+ L"Server version is ")
+ + narrow_to_wide(minetest_version_simple) + L"."
+ );
+ return;
+ }
- // If net_proto_version is set, this client has already been handled
- if(client->getState() > CS_Created)
- {
- verbosestream<<"Server: Ignoring multiple TOSERVER_INITs from "
- <<addr_s<<" (peer_id="<<peer_id<<")"<<std::endl;
- return;
- }
+ client->setPendingSerializationVersion(deployed);
- verbosestream<<"Server: Got TOSERVER_INIT from "<<addr_s<<" (peer_id="
- <<peer_id<<")"<<std::endl;
+ /*
+ Read and check network protocol version
+ */
- // Do not allow multiple players in simple singleplayer mode.
- // This isn't a perfect way to do it, but will suffice for now
- if(m_simple_singleplayer_mode && m_clients.getClientIDs().size() > 1){
- infostream<<"Server: Not allowing another client ("<<addr_s
- <<") to connect in simple singleplayer mode"<<std::endl;
- DenyAccess(peer_id, L"Running in simple singleplayer mode.");
- return;
- }
+ u16 min_net_proto_version = 0;
+ if(pkt->getSize() >= 1 + PLAYERNAME_SIZE + PASSWORD_SIZE + 2)
+ min_net_proto_version = pkt->getU16(1 + PLAYERNAME_SIZE + PASSWORD_SIZE);
+
+ // Use same version as minimum and maximum if maximum version field
+ // doesn't exist (backwards compatibility)
+ u16 max_net_proto_version = min_net_proto_version;
+ if(pkt->getSize() >= 1 + PLAYERNAME_SIZE + PASSWORD_SIZE + 2 + 2)
+ max_net_proto_version = pkt->getU16(1 + PLAYERNAME_SIZE + PASSWORD_SIZE + 2);
+
+ // Start with client's maximum version
+ u16 net_proto_version = max_net_proto_version;
+
+ // Figure out a working version if it is possible at all
+ if(max_net_proto_version >= SERVER_PROTOCOL_VERSION_MIN ||
+ min_net_proto_version <= SERVER_PROTOCOL_VERSION_MAX) {
+ // If maximum is larger than our maximum, go with our maximum
+ if(max_net_proto_version > SERVER_PROTOCOL_VERSION_MAX)
+ net_proto_version = SERVER_PROTOCOL_VERSION_MAX;
+ // Else go with client's maximum
+ else
+ net_proto_version = max_net_proto_version;
+ }
+
+ verbosestream << "Server: " << addr_s << ": Protocol version: min: "
+ << min_net_proto_version << ", max: " << max_net_proto_version
+ << ", chosen: " << net_proto_version << std::endl;
+
+ client->net_proto_version = net_proto_version;
+
+ if(net_proto_version < SERVER_PROTOCOL_VERSION_MIN ||
+ net_proto_version > SERVER_PROTOCOL_VERSION_MAX) {
+ actionstream << "Server: A mismatched client tried to connect from "
+ << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), std::wstring(
+ L"Your client's version is not supported.\n"
+ L"Server version is ")
+ + narrow_to_wide(minetest_version_simple) + L",\n"
+ + L"server's PROTOCOL_VERSION is "
+ + narrow_to_wide(itos(SERVER_PROTOCOL_VERSION_MIN))
+ + L"..."
+ + narrow_to_wide(itos(SERVER_PROTOCOL_VERSION_MAX))
+ + L", client's PROTOCOL_VERSION is "
+ + narrow_to_wide(itos(min_net_proto_version))
+ + L"..."
+ + narrow_to_wide(itos(max_net_proto_version))
+ );
+ return;
+ }
- // First byte after command is maximum supported
- // serialization version
- u8 client_max = data[2];
- u8 our_max = SER_FMT_VER_HIGHEST_READ;
- // Use the highest version supported by both
- int deployed = std::min(client_max, our_max);
- // If it's lower than the lowest supported, give up.
- if(deployed < SER_FMT_CLIENT_VER_LOWEST)
- deployed = SER_FMT_VER_INVALID;
-
- if(deployed == SER_FMT_VER_INVALID)
- {
- actionstream<<"Server: A mismatched client tried to connect from "
- <<addr_s<<std::endl;
- infostream<<"Server: Cannot negotiate serialization version with "
- <<addr_s<<std::endl;
- DenyAccess(peer_id, std::wstring(
+ if(g_settings->getBool("strict_protocol_version_checking")) {
+ if(net_proto_version != LATEST_PROTOCOL_VERSION) {
+ actionstream << "Server: A mismatched (strict) client tried to "
+ << "connect from " << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), std::wstring(
L"Your client's version is not supported.\n"
L"Server version is ")
- + narrow_to_wide(minetest_version_simple) + L"."
+ + narrow_to_wide(minetest_version_simple) + L",\n"
+ + L"server's PROTOCOL_VERSION (strict) is "
+ + narrow_to_wide(itos(LATEST_PROTOCOL_VERSION))
+ + L", client's PROTOCOL_VERSION is "
+ + narrow_to_wide(itos(min_net_proto_version))
+ + L"..."
+ + narrow_to_wide(itos(max_net_proto_version))
);
return;
}
+ }
- client->setPendingSerializationVersion(deployed);
-
- /*
- Read and check network protocol version
- */
-
- u16 min_net_proto_version = 0;
- if(datasize >= 2+1+PLAYERNAME_SIZE+PASSWORD_SIZE+2)
- min_net_proto_version = readU16(&data[2+1+PLAYERNAME_SIZE+PASSWORD_SIZE]);
+ /*
+ Set up player
+ */
+ char playername[PLAYERNAME_SIZE];
+ unsigned int playername_length = 0;
+ for (; playername_length < PLAYERNAME_SIZE; playername_length++ ) {
+ playername[playername_length] = pkt->getChar(1+playername_length);
+ if (pkt->getChar(1+playername_length) == 0)
+ break;
+ }
- // Use same version as minimum and maximum if maximum version field
- // doesn't exist (backwards compatibility)
- u16 max_net_proto_version = min_net_proto_version;
- if(datasize >= 2+1+PLAYERNAME_SIZE+PASSWORD_SIZE+2+2)
- max_net_proto_version = readU16(&data[2+1+PLAYERNAME_SIZE+PASSWORD_SIZE+2]);
+ if (playername_length == PLAYERNAME_SIZE) {
+ actionstream << "Server: Player with name exceeding max length "
+ << "tried to connect from " << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Name too long");
+ return;
+ }
- // Start with client's maximum version
- u16 net_proto_version = max_net_proto_version;
- // Figure out a working version if it is possible at all
- if(max_net_proto_version >= SERVER_PROTOCOL_VERSION_MIN ||
- min_net_proto_version <= SERVER_PROTOCOL_VERSION_MAX)
- {
- // If maximum is larger than our maximum, go with our maximum
- if(max_net_proto_version > SERVER_PROTOCOL_VERSION_MAX)
- net_proto_version = SERVER_PROTOCOL_VERSION_MAX;
- // Else go with client's maximum
- else
- net_proto_version = max_net_proto_version;
- }
+ if(playername[0]=='\0') {
+ actionstream << "Server: Player with an empty name "
+ << "tried to connect from " << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Empty name");
+ return;
+ }
- verbosestream<<"Server: "<<addr_s<<": Protocol version: min: "
- <<min_net_proto_version<<", max: "<<max_net_proto_version
- <<", chosen: "<<net_proto_version<<std::endl;
+ if(string_allowed(playername, PLAYERNAME_ALLOWED_CHARS) == false) {
+ actionstream << "Server: Player with an invalid name "
+ << "tried to connect from " << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Name contains unallowed characters");
+ return;
+ }
- client->net_proto_version = net_proto_version;
+ if(!isSingleplayer() && strcasecmp(playername, "singleplayer") == 0) {
+ actionstream << "Server: Player with the name \"singleplayer\" "
+ << "tried to connect from " << addr_s << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Name is not allowed");
+ return;
+ }
- if(net_proto_version < SERVER_PROTOCOL_VERSION_MIN ||
- net_proto_version > SERVER_PROTOCOL_VERSION_MAX)
- {
- actionstream<<"Server: A mismatched client tried to connect from "
- <<addr_s<<std::endl;
- DenyAccess(peer_id, std::wstring(
- L"Your client's version is not supported.\n"
- L"Server version is ")
- + narrow_to_wide(minetest_version_simple) + L",\n"
- + L"server's PROTOCOL_VERSION is "
- + narrow_to_wide(itos(SERVER_PROTOCOL_VERSION_MIN))
- + L"..."
- + narrow_to_wide(itos(SERVER_PROTOCOL_VERSION_MAX))
- + L", client's PROTOCOL_VERSION is "
- + narrow_to_wide(itos(min_net_proto_version))
- + L"..."
- + narrow_to_wide(itos(max_net_proto_version))
- );
+ {
+ std::string reason;
+ if(m_script->on_prejoinplayer(playername, addr_s, reason)) {
+ actionstream << "Server: Player with the name \"" << playername << "\" "
+ << "tried to connect from " << addr_s << " "
+ << "but it was disallowed for the following reason: "
+ << reason << std::endl;
+ DenyAccess(pkt->getPeerId(), narrow_to_wide(reason.c_str()));
return;
}
+ }
- if(g_settings->getBool("strict_protocol_version_checking"))
- {
- if(net_proto_version != LATEST_PROTOCOL_VERSION)
- {
- actionstream<<"Server: A mismatched (strict) client tried to "
- <<"connect from "<<addr_s<<std::endl;
- DenyAccess(peer_id, std::wstring(
- L"Your client's version is not supported.\n"
- L"Server version is ")
- + narrow_to_wide(minetest_version_simple) + L",\n"
- + L"server's PROTOCOL_VERSION (strict) is "
- + narrow_to_wide(itos(LATEST_PROTOCOL_VERSION))
- + L", client's PROTOCOL_VERSION is "
- + narrow_to_wide(itos(min_net_proto_version))
- + L"..."
- + narrow_to_wide(itos(max_net_proto_version))
- );
- return;
- }
- }
+ infostream<<"Server: New connection: \""<<playername<<"\" from "
+ <<addr_s<<" (peer_id="<<pkt->getPeerId()<<")"<<std::endl;
- /*
- Set up player
- */
- char playername[PLAYERNAME_SIZE];
- unsigned int playername_length = 0;
- for (; playername_length < PLAYERNAME_SIZE; playername_length++ ) {
- playername[playername_length] = data[3+playername_length];
- if (data[3+playername_length] == 0)
- break;
+ // Get password
+ char given_password[PASSWORD_SIZE];
+ if(pkt->getSize() < 1 + PLAYERNAME_SIZE + PASSWORD_SIZE) {
+ // old version - assume blank password
+ given_password[0] = 0;
+ }
+ else {
+ for(u32 i=0; i<PASSWORD_SIZE - 1; i++) {
+ given_password[i] = pkt->getChar(21 + i);
}
+ given_password[PASSWORD_SIZE - 1] = 0;
+ }
- if (playername_length == PLAYERNAME_SIZE) {
- actionstream<<"Server: Player with name exceeding max length "
- <<"tried to connect from "<<addr_s<<std::endl;
- DenyAccess(peer_id, L"Name too long");
- return;
- }
+ if(!base64_is_valid(given_password)){
+ actionstream << "Server: " << playername
+ << " supplied invalid password hash" << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Invalid password hash");
+ return;
+ }
+ // Enforce user limit.
+ // Don't enforce for users that have some admin right
+ if(m_clients.getClientIDs(CS_Created).size() >= g_settings->getU16("max_users") &&
+ !checkPriv(playername, "server") &&
+ !checkPriv(playername, "ban") &&
+ !checkPriv(playername, "privs") &&
+ !checkPriv(playername, "password") &&
+ playername != g_settings->get("name")) {
+ actionstream << "Server: " << playername << " tried to join, but there"
+ << " are already max_users="
+ << g_settings->getU16("max_users") << " players." << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Too many users.");
+ return;
+ }
- if(playername[0]=='\0')
- {
- actionstream<<"Server: Player with an empty name "
- <<"tried to connect from "<<addr_s<<std::endl;
- DenyAccess(peer_id, L"Empty name");
- return;
- }
+ std::string checkpwd; // Password hash to check against
+ bool has_auth = m_script->getAuth(playername, &checkpwd, NULL);
- if(string_allowed(playername, PLAYERNAME_ALLOWED_CHARS)==false)
- {
- actionstream<<"Server: Player with an invalid name "
- <<"tried to connect from "<<addr_s<<std::endl;
- DenyAccess(peer_id, L"Name contains unallowed characters");
+ // If no authentication info exists for user, create it
+ if(!has_auth) {
+ if(!isSingleplayer() &&
+ g_settings->getBool("disallow_empty_password") &&
+ std::string(given_password) == "") {
+ actionstream << "Server: " << playername
+ << " supplied empty password" << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Empty passwords are "
+ L"disallowed. Set a password and try again.");
return;
}
+ std::wstring raw_default_password =
+ narrow_to_wide(g_settings->get("default_password"));
+ std::string initial_password =
+ translatePassword(playername, raw_default_password);
- if(!isSingleplayer() && strcasecmp(playername, "singleplayer") == 0)
- {
- actionstream<<"Server: Player with the name \"singleplayer\" "
- <<"tried to connect from "<<addr_s<<std::endl;
- DenyAccess(peer_id, L"Name is not allowed");
- return;
- }
+ // If default_password is empty, allow any initial password
+ if (raw_default_password.length() == 0)
+ initial_password = given_password;
- {
- std::string reason;
- if(m_script->on_prejoinplayer(playername, addr_s, reason))
- {
- actionstream<<"Server: Player with the name \""<<playername<<"\" "
- <<"tried to connect from "<<addr_s<<" "
- <<"but it was disallowed for the following reason: "
- <<reason<<std::endl;
- DenyAccess(peer_id, narrow_to_wide(reason));
- return;
- }
- }
+ m_script->createAuth(playername, initial_password);
+ }
- infostream<<"Server: New connection: \""<<playername<<"\" from "
- <<addr_s<<" (peer_id="<<peer_id<<")"<<std::endl;
+ has_auth = m_script->getAuth(playername, &checkpwd, NULL);
- // Get password
- char given_password[PASSWORD_SIZE];
- if(datasize < 2+1+PLAYERNAME_SIZE+PASSWORD_SIZE)
- {
- // old version - assume blank password
- given_password[0] = 0;
- }
- else
- {
- for(u32 i=0; i<PASSWORD_SIZE-1; i++)
- {
- given_password[i] = data[23+i];
- }
- given_password[PASSWORD_SIZE-1] = 0;
- }
+ if(!has_auth){
+ actionstream << "Server: " << playername << " cannot be authenticated"
+ << " (auth handler does not work?)" << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Not allowed to login");
+ return;
+ }
- if(!base64_is_valid(given_password)){
- actionstream<<"Server: "<<playername
- <<" supplied invalid password hash"<<std::endl;
- DenyAccess(peer_id, L"Invalid password hash");
- return;
- }
+ if(given_password != checkpwd) {
+ actionstream << "Server: " << playername << " supplied wrong password"
+ << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Wrong password");
+ return;
+ }
- // Enforce user limit.
- // Don't enforce for users that have some admin right
- if(m_clients.getClientIDs(CS_Created).size() >= g_settings->getU16("max_users") &&
- !checkPriv(playername, "server") &&
- !checkPriv(playername, "ban") &&
- !checkPriv(playername, "privs") &&
- !checkPriv(playername, "password") &&
- playername != g_settings->get("name"))
- {
- actionstream<<"Server: "<<playername<<" tried to join, but there"
- <<" are already max_users="
- <<g_settings->getU16("max_users")<<" players."<<std::endl;
- DenyAccess(peer_id, L"Too many users.");
- return;
- }
+ RemotePlayer *player =
+ static_cast<RemotePlayer*>(m_env->getPlayer(playername));
- std::string checkpwd; // Password hash to check against
- bool has_auth = m_script->getAuth(playername, &checkpwd, NULL);
-
- // If no authentication info exists for user, create it
- if(!has_auth){
- if(!isSingleplayer() &&
- g_settings->getBool("disallow_empty_password") &&
- std::string(given_password) == ""){
- actionstream<<"Server: "<<playername
- <<" supplied empty password"<<std::endl;
- DenyAccess(peer_id, L"Empty passwords are "
- L"disallowed. Set a password and try again.");
- return;
- }
- std::wstring raw_default_password =
- narrow_to_wide(g_settings->get("default_password"));
- std::string initial_password =
- translatePassword(playername, raw_default_password);
+ if(player && player->peer_id != 0) {
+ errorstream << "Server: " << playername << ": Failed to emerge player"
+ << " (player allocated to an another client)" << std::endl;
+ DenyAccess(pkt->getPeerId(), L"Another client is connected with this "
+ L"name. If your client closed unexpectedly, try again in "
+ L"a minute.");
+ }
- // If default_password is empty, allow any initial password
- if (raw_default_password.length() == 0)
- initial_password = given_password;
+ m_clients.setPlayerName(pkt->getPeerId(), playername);
- m_script->createAuth(playername, initial_password);
- }
+ /*
+ Answer with a TOCLIENT_INIT
+ */
+ {
+ SharedBuffer<u8> reply(2 + 1 + 6 + 8 + 4);
+ writeU16(&reply[0], TOCLIENT_INIT);
+ writeU8(&reply[2], deployed);
+ //send dummy pos for legacy reasons only
+ writeV3S16(&reply[2 + 1], floatToInt(v3f(0,0,0), BS));
+ writeU64(&reply[2 + 1 + 6], m_env->getServerMap().getSeed());
+ writeF1000(&reply[2 + 1 + 6 + 8], g_settings->getFloat("dedicated_server_step"));
- has_auth = m_script->getAuth(playername, &checkpwd, NULL);
+ // Send as reliable
+ m_clients.send(pkt->getPeerId(), 0, reply, true);
+ m_clients.event(pkt->getPeerId(), CSE_Init);
+ }
+}
- if(!has_auth){
- actionstream<<"Server: "<<playername<<" cannot be authenticated"
- <<" (auth handler does not work?)"<<std::endl;
- DenyAccess(peer_id, L"Not allowed to login");
- return;
- }
+void Server::handleCommand_Init2(ToServerPacket* pkt)
+{
+ verbosestream << "Server: Got TOSERVER_INIT2 from "
+ << pkt->getPeerId() << std::endl;
- if(given_password != checkpwd){
- actionstream<<"Server: "<<playername<<" supplied wrong password"
- <<std::endl;
- DenyAccess(peer_id, L"Wrong password");
+ m_clients.event(pkt->getPeerId(), CSE_GotInit2);
+ u16 protocol_version = m_clients.getProtocolVersion(pkt->getPeerId());
+
+
+ ///// begin compatibility code
+ PlayerSAO* playersao = NULL;
+ if (protocol_version <= 22) {
+ playersao = StageTwoClientInit(pkt->getPeerId());
+
+ if (playersao == NULL) {
+ errorstream
+ << "TOSERVER_INIT2 stage 2 client init failed for peer "
+ << pkt->getPeerId() << std::endl;
return;
}
+ }
+ ///// end compatibility code
- RemotePlayer *player =
- static_cast<RemotePlayer*>(m_env->getPlayer(playername));
+ /*
+ Send some initialization data
+ */
- if(player && player->peer_id != 0){
- errorstream<<"Server: "<<playername<<": Failed to emerge player"
- <<" (player allocated to an another client)"<<std::endl;
- DenyAccess(peer_id, L"Another client is connected with this "
- L"name. If your client closed unexpectedly, try again in "
- L"a minute.");
- }
+ infostream << "Server: Sending content to "
+ << getPlayerName(pkt->getPeerId()) << std::endl;
- m_clients.setPlayerName(peer_id,playername);
+ // Send player movement settings
+ SendMovement(pkt->getPeerId());
- /*
- Answer with a TOCLIENT_INIT
- */
- {
- SharedBuffer<u8> reply(2+1+6+8+4);
- writeU16(&reply[0], TOCLIENT_INIT);
- writeU8(&reply[2], deployed);
- //send dummy pos for legacy reasons only
- writeV3S16(&reply[2+1], floatToInt(v3f(0,0,0), BS));
- writeU64(&reply[2+1+6], m_env->getServerMap().getSeed());
- writeF1000(&reply[2+1+6+8], g_settings->getFloat("dedicated_server_step"));
+ // Send item definitions
+ SendItemDef(pkt->getPeerId(), m_itemdef, protocol_version);
- // Send as reliable
- m_clients.send(peer_id, 0, reply, true);
- m_clients.event(peer_id, CSE_Init);
- }
+ // Send node definitions
+ SendNodeDef(pkt->getPeerId(), m_nodedef, protocol_version);
- return;
+ m_clients.event(pkt->getPeerId(), CSE_SetDefinitionsSent);
+
+ // Send media announcement
+ sendMediaAnnouncement(pkt->getPeerId());
+
+ // Send detached inventories
+ sendDetachedInventories(pkt->getPeerId());
+
+ // Send time of day
+ u16 time = m_env->getTimeOfDay();
+ float time_speed = g_settings->getFloat("time_speed");
+ SendTimeOfDay(pkt->getPeerId(), time, time_speed);
+
+ ///// begin compatibility code
+ if (protocol_version <= 22) {
+ m_clients.event(pkt->getPeerId(), CSE_SetClientReady);
+ m_script->on_joinplayer(playersao);
}
+ ///// end compatibility code
- if(command == TOSERVER_INIT2)
- {
+ // Warnings about protocol version can be issued here
+ if(getClient(pkt->getPeerId())->net_proto_version < LATEST_PROTOCOL_VERSION) {
+ SendChatMessage(pkt->getPeerId(), L"# Server: WARNING: YOUR CLIENT'S "
+ L"VERSION MAY NOT BE FULLY COMPATIBLE WITH THIS SERVER!");
+ }
+}
- verbosestream<<"Server: Got TOSERVER_INIT2 from "
- <<peer_id<<std::endl;
+void Server::handleCommand_RequestMedia(ToServerPacket* pkt)
+{
+ std::list<std::string> tosend;
+ u16 numfiles;
- m_clients.event(peer_id, CSE_GotInit2);
- u16 protocol_version = m_clients.getProtocolVersion(peer_id);
+ *pkt >> numfiles;
+ infostream << "Sending " << numfiles << " files to "
+ << getPlayerName(pkt->getPeerId()) << std::endl;
+ verbosestream << "TOSERVER_REQUEST_MEDIA: " << std::endl;
- ///// begin compatibility code
- PlayerSAO* playersao = NULL;
- if (protocol_version <= 22) {
- playersao = StageTwoClientInit(peer_id);
+ for(int i = 0; i < numfiles; i++) {
+ std::string name;
- if (playersao == NULL) {
- errorstream
- << "TOSERVER_INIT2 stage 2 client init failed for peer "
- << peer_id << std::endl;
- return;
- }
- }
- ///// end compatibility code
+ *pkt >> name;
- /*
- Send some initialization data
- */
+ tosend.push_back(name);
+ verbosestream << "TOSERVER_REQUEST_MEDIA: requested file "
+ << name << std::endl;
+ }
- infostream<<"Server: Sending content to "
- <<getPlayerName(peer_id)<<std::endl;
+ sendRequestedMedia(pkt->getPeerId(), tosend);
+}
- // Send player movement settings
- SendMovement(peer_id);
+void Server::handleCommand_ReceivedMedia(ToServerPacket* pkt)
+{
+}
- // Send item definitions
- SendItemDef(peer_id, m_itemdef, protocol_version);
+void Server::handleCommand_ClientReady(ToServerPacket* pkt)
+{
+ u16 peer_id = pkt->getPeerId();
+ u16 peer_proto_ver = getClient(peer_id, CS_InitDone)->net_proto_version;
- // Send node definitions
- SendNodeDef(peer_id, m_nodedef, protocol_version);
+ // clients <= protocol version 22 did not send ready message,
+ // they're already initialized
+ if (peer_proto_ver <= 22) {
+ infostream << "Client sent message not expected by a "
+ << "client using protocol version <= 22,"
+ << "disconnecing peer_id: " << peer_id << std::endl;
+ m_con.DisconnectPeer(peer_id);
+ return;
+ }
- m_clients.event(peer_id, CSE_SetDefinitionsSent);
+ PlayerSAO* playersao = StageTwoClientInit(peer_id);
- // Send media announcement
- sendMediaAnnouncement(peer_id);
+ if (playersao == NULL) {
+ errorstream
+ << "TOSERVER_CLIENT_READY stage 2 client init failed for peer_id: "
+ << peer_id << std::endl;
+ m_con.DisconnectPeer(peer_id);
+ return;
+ }
- // Send detached inventories
- sendDetachedInventories(peer_id);
- // Send time of day
- u16 time = m_env->getTimeOfDay();
- float time_speed = g_settings->getFloat("time_speed");
- SendTimeOfDay(peer_id, time, time_speed);
+ if(pkt->getSize() < 8) {
+ errorstream
+ << "TOSERVER_CLIENT_READY client sent inconsistent data, disconnecting peer_id: "
+ << peer_id << std::endl;
+ m_con.DisconnectPeer(peer_id);
+ return;
+ }
- ///// begin compatibility code
- if (protocol_version <= 22) {
- m_clients.event(peer_id, CSE_SetClientReady);
- m_script->on_joinplayer(playersao);
- }
- ///// end compatibility code
+ u8 major_ver, minor_ver, patch_ver;
+ *pkt >> major_ver >> minor_ver >> patch_ver;
- // Warnings about protocol version can be issued here
- if(getClient(peer_id)->net_proto_version < LATEST_PROTOCOL_VERSION)
- {
- SendChatMessage(peer_id, L"# Server: WARNING: YOUR CLIENT'S "
- L"VERSION MAY NOT BE FULLY COMPATIBLE WITH THIS SERVER!");
- }
+ m_clients.setClientVersion(
+ peer_id, major_ver, minor_ver, patch_ver,
+ std::string(pkt->getString(6),(u16) pkt->getU8(4)));
+ m_clients.event(peer_id, CSE_SetClientReady);
+ m_script->on_joinplayer(playersao);
+}
+
+void Server::handleCommand_GotBlocks(ToServerPacket* pkt)
+{
+ if(pkt->getSize() < 1)
return;
+
+ /*
+ [0] u16 command
+ [2] u8 count
+ [3] v3s16 pos_0
+ [3+6] v3s16 pos_1
+ ...
+ */
+
+ u8 count;
+ *pkt >> count;
+
+ RemoteClient *client = getClient(pkt->getPeerId());
+
+ for(u16 i=0; i<count; i++) {
+ if((s16)pkt->getSize() < 1 + (i + 1) * 6)
+ throw con::InvalidIncomingDataException
+ ("GOTBLOCKS length is too short");
+ v3s16 p;
+
+ *pkt >> p;
+
+ client->GotBlock(p);
}
+}
- u8 peer_ser_ver = getClient(peer_id, CS_InitDone)->serialization_version;
- u16 peer_proto_ver = getClient(peer_id, CS_InitDone)->net_proto_version;
+void Server::handleCommand_PlayerPos(ToServerPacket* pkt)
+{
+ if(pkt->getSize() < 12 + 12 + 4 + 4)
+ return;
- if(peer_ser_ver == SER_FMT_VER_INVALID)
- {
- errorstream<<"Server::ProcessData(): Cancelling: Peer"
- " serialization format invalid or not initialized."
- " Skipping incoming command="<<command<<std::endl;
+ v3s32 ps, ss;
+ s32 f32pitch, f32yaw;
+
+ *pkt >> ps;
+ *pkt >> ss;
+ *pkt >> f32pitch;
+ *pkt >> f32yaw;
+
+ f32 pitch = (f32)f32pitch / 100.0;
+ f32 yaw = (f32)f32yaw / 100.0;
+ u32 keyPressed = 0;
+
+ if(pkt->getSize() >= 12 + 12 + 4 + 4 + 4)
+ *pkt >> keyPressed;
+
+ v3f position((f32)ps.X / 100.0, (f32)ps.Y / 100.0, (f32)ps.Z / 100.0);
+ v3f speed((f32)ss.X / 100.0, (f32)ss.Y / 100.0, (f32)ss.Z / 100.0);
+ pitch = wrapDegrees(pitch);
+ yaw = wrapDegrees(yaw);
+
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- /* Handle commands relate to client startup */
- if(command == TOSERVER_REQUEST_MEDIA) {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- std::list<std::string> tosend;
- u16 numfiles = readU16(is);
+ player->setPosition(position);
+ player->setSpeed(speed);
+ player->setPitch(pitch);
+ player->setYaw(yaw);
+ player->keyPressed=keyPressed;
+ player->control.up = (bool)(keyPressed & 1);
+ player->control.down = (bool)(keyPressed & 2);
+ player->control.left = (bool)(keyPressed & 4);
+ player->control.right = (bool)(keyPressed & 8);
+ player->control.jump = (bool)(keyPressed & 16);
+ player->control.aux1 = (bool)(keyPressed & 32);
+ player->control.sneak = (bool)(keyPressed & 64);
+ player->control.LMB = (bool)(keyPressed & 128);
+ player->control.RMB = (bool)(keyPressed & 256);
+
+ bool cheated = playersao->checkMovementCheat();
+ if(cheated) {
+ // Call callbacks
+ m_script->on_cheat(playersao, "moved_too_fast");
+ }
+}
- infostream<<"Sending "<<numfiles<<" files to "
- <<getPlayerName(peer_id)<<std::endl;
- verbosestream<<"TOSERVER_REQUEST_MEDIA: "<<std::endl;
+void Server::handleCommand_DeletedBlocks(ToServerPacket* pkt)
+{
+ if(pkt->getSize() < 1)
+ return;
- for(int i = 0; i < numfiles; i++) {
- std::string name = deSerializeString(is);
- tosend.push_back(name);
- verbosestream<<"TOSERVER_REQUEST_MEDIA: requested file "
- <<name<<std::endl;
- }
+ /*
+ [0] u16 command
+ [2] u8 count
+ [3] v3s16 pos_0
+ [3+6] v3s16 pos_1
+ ...
+ */
+
+ u8 count;
+ *pkt >> count;
+
+ RemoteClient *client = getClient(pkt->getPeerId());
+
+ for(u16 i=0; i<count; i++) {
+ if((s16)pkt->getSize() < 1 + (i + 1) * 6)
+ throw con::InvalidIncomingDataException
+ ("DELETEDBLOCKS length is too short");
+ v3s16 p;
+ *pkt >> p;
+
+ client->SetBlockNotSent(p);
+ }
+}
- sendRequestedMedia(peer_id, tosend);
+void Server::handleCommand_InventoryAction(ToServerPacket* pkt)
+{
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- else if(command == TOSERVER_RECEIVED_MEDIA) {
+
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
+
+ // Strip command and create a stream
+ std::string datastring(pkt->getString(0), pkt->getSize());
+ verbosestream << "TOSERVER_INVENTORY_ACTION: data=" << datastring
+ << std::endl;
+ std::istringstream is(datastring, std::ios_base::binary);
+ // Create an action
+ InventoryAction *a = InventoryAction::deSerialize(is);
+ if(a == NULL) {
+ infostream << "TOSERVER_INVENTORY_ACTION: "
+ << "InventoryAction::deSerialize() returned NULL"
+ << std::endl;
return;
}
- else if(command == TOSERVER_CLIENT_READY) {
- // clients <= protocol version 22 did not send ready message,
- // they're already initialized
- if (peer_proto_ver <= 22) {
- infostream << "Client sent message not expected by a "
- << "client using protocol version <= 22,"
- << "disconnecing peer_id: " << peer_id << std::endl;
- m_con.DisconnectPeer(peer_id);
+
+ // If something goes wrong, this player is to blame
+ RollbackScopeActor rollback_scope(m_rollback,
+ std::string("player:")+player->getName());
+
+ /*
+ Note: Always set inventory not sent, to repair cases
+ where the client made a bad prediction.
+ */
+
+ /*
+ Handle restrictions and special cases of the move action
+ */
+ if(a->getType() == IACTION_MOVE) {
+ IMoveAction *ma = (IMoveAction*)a;
+
+ ma->from_inv.applyCurrentPlayer(player->getName());
+ ma->to_inv.applyCurrentPlayer(player->getName());
+
+ setInventoryModified(ma->from_inv);
+ setInventoryModified(ma->to_inv);
+
+ bool from_inv_is_current_player =
+ (ma->from_inv.type == InventoryLocation::PLAYER) &&
+ (ma->from_inv.name == player->getName());
+
+ bool to_inv_is_current_player =
+ (ma->to_inv.type == InventoryLocation::PLAYER) &&
+ (ma->to_inv.name == player->getName());
+
+ /*
+ Disable moving items out of craftpreview
+ */
+ if(ma->from_list == "craftpreview") {
+ infostream << "Ignoring IMoveAction from "
+ << (ma->from_inv.dump()) << ":" << ma->from_list
+ << " to " << (ma->to_inv.dump()) << ":" << ma->to_list
+ << " because src is " << ma->from_list << std::endl;
+ delete a;
return;
}
- PlayerSAO* playersao = StageTwoClientInit(peer_id);
+ /*
+ Disable moving items into craftresult and craftpreview
+ */
+ if(ma->to_list == "craftpreview" || ma->to_list == "craftresult") {
+ infostream << "Ignoring IMoveAction from "
+ << (ma->from_inv.dump()) << ":" << ma->from_list
+ << " to " << (ma->to_inv.dump()) << ":" << ma->to_list
+ << " because dst is " << ma->to_list << std::endl;
+ delete a;
+ return;
+ }
- if (playersao == NULL) {
- errorstream
- << "TOSERVER_CLIENT_READY stage 2 client init failed for peer_id: "
- << peer_id << std::endl;
- m_con.DisconnectPeer(peer_id);
+ // Disallow moving items in elsewhere than player's inventory
+ // if not allowed to interact
+ if(!checkPriv(player->getName(), "interact") &&
+ (!from_inv_is_current_player ||
+ !to_inv_is_current_player)) {
+ infostream << "Cannot move outside of player's inventory: "
+ << "No interact privilege" << std::endl;
+ delete a;
return;
}
+ }
+ /*
+ Handle restrictions and special cases of the drop action
+ */
+ else if(a->getType() == IACTION_DROP) {
+ IDropAction *da = (IDropAction*)a;
+ da->from_inv.applyCurrentPlayer(player->getName());
- if(datasize < 2+8) {
- errorstream
- << "TOSERVER_CLIENT_READY client sent inconsistent data, disconnecting peer_id: "
- << peer_id << std::endl;
- m_con.DisconnectPeer(peer_id);
+ setInventoryModified(da->from_inv);
+
+ /*
+ Disable dropping items out of craftpreview
+ */
+ if(da->from_list == "craftpreview") {
+ infostream << "Ignoring IDropAction from "
+ << (da->from_inv.dump()) << ":" << da->from_list
+ << " because src is " << da->from_list << std::endl;
+ delete a;
return;
}
- m_clients.setClientVersion(
- peer_id,
- data[2], data[3], data[4],
- std::string((char*) &data[8],(u16) data[6]));
+ // Disallow dropping items if not allowed to interact
+ if(!checkPriv(player->getName(), "interact")) {
+ delete a;
+ return;
+ }
+ }
+ /*
+ Handle restrictions and special cases of the craft action
+ */
+ else if(a->getType() == IACTION_CRAFT) {
+ ICraftAction *ca = (ICraftAction*)a;
- m_clients.event(peer_id, CSE_SetClientReady);
- m_script->on_joinplayer(playersao);
+ ca->craft_inv.applyCurrentPlayer(player->getName());
- }
- else if(command == TOSERVER_GOTBLOCKS)
- {
- if(datasize < 2+1)
- return;
+ setInventoryModified(ca->craft_inv);
- /*
- [0] u16 command
- [2] u8 count
- [3] v3s16 pos_0
- [3+6] v3s16 pos_1
- ...
- */
+ //bool craft_inv_is_current_player =
+ // (ca->craft_inv.type == InventoryLocation::PLAYER) &&
+ // (ca->craft_inv.name == player->getName());
- u16 count = data[2];
- for(u16 i=0; i<count; i++)
- {
- if((s16)datasize < 2+1+(i+1)*6)
- throw con::InvalidIncomingDataException
- ("GOTBLOCKS length is too short");
- v3s16 p = readV3S16(&data[2+1+i*6]);
- /*infostream<<"Server: GOTBLOCKS ("
- <<p.X<<","<<p.Y<<","<<p.Z<<")"<<std::endl;*/
- RemoteClient *client = getClient(peer_id);
- client->GotBlock(p);
+ // Disallow crafting if not allowed to interact
+ if(!checkPriv(player->getName(), "interact")) {
+ infostream << "Cannot craft: "
+ << "No interact privilege" << std::endl;
+ delete a;
+ return;
}
- return;
}
- if (m_clients.getClientState(peer_id) < CS_Active)
- {
- if (command == TOSERVER_PLAYERPOS) return;
+ // Do the action
+ a->apply(this, playersao, this);
+ // Eat the action
+ delete a;
+}
- errorstream<<"Got packet command: " << command << " for peer id "
- << peer_id << " but client isn't active yet. Dropping packet "
- <<std::endl;
- return;
+void Server::handleCommand_ChatMessage(ToServerPacket* pkt)
+{
+ /*
+ u16 command
+ u16 length
+ wstring message
+ */
+ u16 len;
+ *pkt >> len;
+
+ std::wstring message;
+ for(u16 i=0; i<len; i++) {
+ u16 tmp_wchar;
+ *pkt >> tmp_wchar;
+
+ message += (wchar_t)tmp_wchar;
}
- Player *player = m_env->getPlayer(peer_id);
+ Player *player = m_env->getPlayer(pkt->getPeerId());
if(player == NULL) {
- errorstream<<"Server::ProcessData(): Cancelling: "
- "No player for peer_id="<<peer_id
- << " disconnecting peer!" <<std::endl;
- m_con.DisconnectPeer(peer_id);
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- PlayerSAO *playersao = player->getPlayerSAO();
- if(playersao == NULL) {
- errorstream<<"Server::ProcessData(): Cancelling: "
- "No player object for peer_id="<<peer_id
- << " disconnecting peer!" <<std::endl;
- m_con.DisconnectPeer(peer_id);
- return;
- }
+ // If something goes wrong, this player is to blame
+ RollbackScopeActor rollback_scope(m_rollback,
+ std::string("player:")+player->getName());
- if(command == TOSERVER_PLAYERPOS)
- {
- if(datasize < 2+12+12+4+4)
- return;
+ // Get player name of this client
+ std::wstring name = narrow_to_wide(player->getName());
- u32 start = 0;
- v3s32 ps = readV3S32(&data[start+2]);
- v3s32 ss = readV3S32(&data[start+2+12]);
- f32 pitch = (f32)readS32(&data[2+12+12]) / 100.0;
- f32 yaw = (f32)readS32(&data[2+12+12+4]) / 100.0;
- u32 keyPressed = 0;
- if(datasize >= 2+12+12+4+4+4)
- keyPressed = (u32)readU32(&data[2+12+12+4+4]);
- v3f position((f32)ps.X/100., (f32)ps.Y/100., (f32)ps.Z/100.);
- v3f speed((f32)ss.X/100., (f32)ss.Y/100., (f32)ss.Z/100.);
- pitch = wrapDegrees(pitch);
- yaw = wrapDegrees(yaw);
-
- player->setPosition(position);
- player->setSpeed(speed);
- player->setPitch(pitch);
- player->setYaw(yaw);
- player->keyPressed=keyPressed;
- player->control.up = (bool)(keyPressed&1);
- player->control.down = (bool)(keyPressed&2);
- player->control.left = (bool)(keyPressed&4);
- player->control.right = (bool)(keyPressed&8);
- player->control.jump = (bool)(keyPressed&16);
- player->control.aux1 = (bool)(keyPressed&32);
- player->control.sneak = (bool)(keyPressed&64);
- player->control.LMB = (bool)(keyPressed&128);
- player->control.RMB = (bool)(keyPressed&256);
-
- bool cheated = playersao->checkMovementCheat();
- if(cheated){
- // Call callbacks
- m_script->on_cheat(playersao, "moved_too_fast");
- }
+ // Run script hook
+ bool ate = m_script->on_chat_message(player->getName(),
+ wide_to_narrow(message));
+ // If script ate the message, don't proceed
+ if(ate)
+ return;
- /*infostream<<"Server::ProcessData(): Moved player "<<peer_id<<" to "
- <<"("<<position.X<<","<<position.Y<<","<<position.Z<<")"
- <<" pitch="<<pitch<<" yaw="<<yaw<<std::endl;*/
+ // Line to send to players
+ std::wstring line;
+ // Whether to send to the player that sent the line
+ bool send_to_sender_only = false;
+
+ // Commands are implemented in Lua, so only catch invalid
+ // commands that were not "eaten" and send an error back
+ if(message[0] == L'/') {
+ message = message.substr(1);
+ send_to_sender_only = true;
+ if(message.length() == 0)
+ line += L"-!- Empty command";
+ else
+ line += L"-!- Invalid command: " + str_split(message, L' ')[0];
+ }
+ else {
+ if(checkPriv(player->getName(), "shout")) {
+ line += L"<";
+ line += name;
+ line += L"> ";
+ line += message;
+ } else {
+ line += L"-!- You don't have permission to shout.";
+ send_to_sender_only = true;
+ }
}
- else if(command == TOSERVER_DELETEDBLOCKS)
- {
- if(datasize < 2+1)
- return;
+ if(line != L"")
+ {
/*
- [0] u16 command
- [2] u8 count
- [3] v3s16 pos_0
- [3+6] v3s16 pos_1
- ...
+ Send the message to sender
*/
+ if (send_to_sender_only) {
+ SendChatMessage(pkt->getPeerId(), line);
+ }
+ /*
+ Send the message to others
+ */
+ else {
+ actionstream << "CHAT: " << wide_to_narrow(line)<<std::endl;
- u16 count = data[2];
- for(u16 i=0; i<count; i++)
- {
- if((s16)datasize < 2+1+(i+1)*6)
- throw con::InvalidIncomingDataException
- ("DELETEDBLOCKS length is too short");
- v3s16 p = readV3S16(&data[2+1+i*6]);
- /*infostream<<"Server: DELETEDBLOCKS ("
- <<p.X<<","<<p.Y<<","<<p.Z<<")"<<std::endl;*/
- RemoteClient *client = getClient(peer_id);
- client->SetBlockNotSent(p);
+ std::list<u16> clients = m_clients.getClientIDs();
+
+ for(std::list<u16>::iterator
+ i = clients.begin();
+ i != clients.end(); ++i) {
+ if (*i != pkt->getPeerId())
+ SendChatMessage(*i, line);
+ }
}
}
- else if(command == TOSERVER_CLICK_OBJECT)
- {
- infostream<<"Server: CLICK_OBJECT not supported anymore"<<std::endl;
+}
+
+void Server::handleCommand_Damage(ToServerPacket* pkt)
+{
+ u8 damage;
+
+ *pkt >> damage;
+
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- else if(command == TOSERVER_CLICK_ACTIVEOBJECT)
- {
- infostream<<"Server: CLICK_ACTIVEOBJECT not supported anymore"<<std::endl;
+
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- else if(command == TOSERVER_GROUND_ACTION)
- {
- infostream<<"Server: GROUND_ACTION not supported anymore"<<std::endl;
- return;
+ if(g_settings->getBool("enable_damage")) {
+ actionstream << player->getName() << " damaged by "
+ << (int)damage << " hp at " << PP(player->getPosition() / BS)
+ << std::endl;
+
+ playersao->setHP(playersao->getHP() - damage);
+
+ if(playersao->getHP() == 0 && playersao->m_hp_not_sent)
+ DiePlayer(pkt->getPeerId());
+
+ if(playersao->m_hp_not_sent)
+ SendPlayerHP(pkt->getPeerId());
}
- else if(command == TOSERVER_RELEASE)
- {
- infostream<<"Server: RELEASE not supported anymore"<<std::endl;
- return;
- }
- else if(command == TOSERVER_SIGNTEXT)
- {
- infostream<<"Server: SIGNTEXT not supported anymore"
- <<std::endl;
+}
+
+void Server::handleCommand_Breath(ToServerPacket* pkt)
+{
+ u16 breath;
+
+ *pkt >> breath;
+
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- else if(command == TOSERVER_SIGNNODETEXT)
- {
- infostream<<"Server: SIGNNODETEXT not supported anymore"
- <<std::endl;
+
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
return;
}
- else if(command == TOSERVER_INVENTORY_ACTION)
- {
- // Strip command and create a stream
- std::string datastring((char*)&data[2], datasize-2);
- verbosestream<<"TOSERVER_INVENTORY_ACTION: data="<<datastring<<std::endl;
- std::istringstream is(datastring, std::ios_base::binary);
- // Create an action
- InventoryAction *a = InventoryAction::deSerialize(is);
- if(a == NULL)
- {
- infostream<<"TOSERVER_INVENTORY_ACTION: "
- <<"InventoryAction::deSerialize() returned NULL"
- <<std::endl;
- return;
- }
- // If something goes wrong, this player is to blame
- RollbackScopeActor rollback_scope(m_rollback,
- std::string("player:")+player->getName());
+ playersao->setBreath(breath);
+ m_script->player_event(playersao,"breath_changed");
+}
- /*
- Note: Always set inventory not sent, to repair cases
- where the client made a bad prediction.
- */
+void Server::handleCommand_Password(ToServerPacket* pkt)
+{
+ /*
+ [0] u16 TOSERVER_PASSWORD
+ [2] u8[28] old password
+ [30] u8[28] new password
+ */
- /*
- Handle restrictions and special cases of the move action
- */
- if(a->getType() == IACTION_MOVE)
- {
- IMoveAction *ma = (IMoveAction*)a;
+ if(pkt->getSize() != PASSWORD_SIZE * 2)
+ return;
- ma->from_inv.applyCurrentPlayer(player->getName());
- ma->to_inv.applyCurrentPlayer(player->getName());
+ std::string oldpwd;
+ std::string newpwd;
- setInventoryModified(ma->from_inv);
- setInventoryModified(ma->to_inv);
+ for(u32 i=0; i<PASSWORD_SIZE - 1; i++) {
+ char c = pkt->getChar(i);
+ if(c == 0)
+ break;
+ oldpwd += c;
+ }
- bool from_inv_is_current_player =
- (ma->from_inv.type == InventoryLocation::PLAYER) &&
- (ma->from_inv.name == player->getName());
+ for(u32 i=0; i<PASSWORD_SIZE - 1; i++) {
+ char c = pkt->getChar(PASSWORD_SIZE + i);
+ if(c == 0)
+ break;
+ newpwd += c;
+ }
- bool to_inv_is_current_player =
- (ma->to_inv.type == InventoryLocation::PLAYER) &&
- (ma->to_inv.name == player->getName());
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- /*
- Disable moving items out of craftpreview
- */
- if(ma->from_list == "craftpreview")
- {
- infostream<<"Ignoring IMoveAction from "
- <<(ma->from_inv.dump())<<":"<<ma->from_list
- <<" to "<<(ma->to_inv.dump())<<":"<<ma->to_list
- <<" because src is "<<ma->from_list<<std::endl;
- delete a;
- return;
- }
+ if(!base64_is_valid(newpwd)) {
+ infostream<<"Server: " << player->getName() <<
+ " supplied invalid password hash" << std::endl;
+ // Wrong old password supplied!!
+ SendChatMessage(pkt->getPeerId(), L"Invalid new password hash supplied. Password NOT changed.");
+ return;
+ }
- /*
- Disable moving items into craftresult and craftpreview
- */
- if(ma->to_list == "craftpreview" || ma->to_list == "craftresult")
- {
- infostream<<"Ignoring IMoveAction from "
- <<(ma->from_inv.dump())<<":"<<ma->from_list
- <<" to "<<(ma->to_inv.dump())<<":"<<ma->to_list
- <<" because dst is "<<ma->to_list<<std::endl;
- delete a;
- return;
- }
+ infostream << "Server: Client requests a password change from "
+ << "'" << oldpwd << "' to '" << newpwd << "'" << std::endl;
- // Disallow moving items in elsewhere than player's inventory
- // if not allowed to interact
- if(!checkPriv(player->getName(), "interact") &&
- (!from_inv_is_current_player ||
- !to_inv_is_current_player))
- {
- infostream<<"Cannot move outside of player's inventory: "
- <<"No interact privilege"<<std::endl;
- delete a;
- return;
- }
- }
- /*
- Handle restrictions and special cases of the drop action
- */
- else if(a->getType() == IACTION_DROP)
- {
- IDropAction *da = (IDropAction*)a;
+ std::string playername = player->getName();
- da->from_inv.applyCurrentPlayer(player->getName());
+ std::string checkpwd;
+ m_script->getAuth(playername, &checkpwd, NULL);
- setInventoryModified(da->from_inv);
+ if(oldpwd != checkpwd) {
+ infostream << "Server: invalid old password" << std::endl;
+ // Wrong old password supplied!!
+ SendChatMessage(pkt->getPeerId(), L"Invalid old password supplied. Password NOT changed.");
+ return;
+ }
- /*
- Disable dropping items out of craftpreview
- */
- if(da->from_list == "craftpreview")
- {
- infostream<<"Ignoring IDropAction from "
- <<(da->from_inv.dump())<<":"<<da->from_list
- <<" because src is "<<da->from_list<<std::endl;
- delete a;
- return;
- }
+ bool success = m_script->setPassword(playername, newpwd);
+ if(success) {
+ actionstream << player->getName() << " changes password" << std::endl;
+ SendChatMessage(pkt->getPeerId(), L"Password change successful.");
+ } else {
+ actionstream << player->getName() << " tries to change password but "
+ << "it fails" << std::endl;
+ SendChatMessage(pkt->getPeerId(), L"Password change failed or inavailable.");
+ }
+}
- // Disallow dropping items if not allowed to interact
- if(!checkPriv(player->getName(), "interact"))
- {
- delete a;
- return;
- }
- }
- /*
- Handle restrictions and special cases of the craft action
- */
- else if(a->getType() == IACTION_CRAFT)
- {
- ICraftAction *ca = (ICraftAction*)a;
+void Server::handleCommand_PlayerItem(ToServerPacket* pkt)
+{
+ if (pkt->getSize() < 2)
+ return;
- ca->craft_inv.applyCurrentPlayer(player->getName());
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- setInventoryModified(ca->craft_inv);
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- //bool craft_inv_is_current_player =
- // (ca->craft_inv.type == InventoryLocation::PLAYER) &&
- // (ca->craft_inv.name == player->getName());
+ u16 item;
- // Disallow crafting if not allowed to interact
- if(!checkPriv(player->getName(), "interact"))
- {
- infostream<<"Cannot craft: "
- <<"No interact privilege"<<std::endl;
- delete a;
- return;
- }
- }
+ *pkt >> item;
- // Do the action
- a->apply(this, playersao, this);
- // Eat the action
- delete a;
+ playersao->setWieldIndex(item);
+}
+
+void Server::handleCommand_Respawn(ToServerPacket* pkt)
+{
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
}
- else if(command == TOSERVER_CHAT_MESSAGE)
- {
- /*
- u16 command
- u16 length
- wstring message
- */
- u8 buf[6];
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
- // Read stuff
- is.read((char*)buf, 2);
- u16 len = readU16(buf);
+ if(player->hp != 0 || !g_settings->getBool("enable_damage"))
+ return;
- std::wstring message;
- for(u16 i=0; i<len; i++)
- {
- is.read((char*)buf, 2);
- message += (wchar_t)readU16(buf);
- }
+ RespawnPlayer(pkt->getPeerId());
- // If something goes wrong, this player is to blame
- RollbackScopeActor rollback_scope(m_rollback,
- std::string("player:")+player->getName());
+ actionstream<<player->getName()<<" respawns at "
+ <<PP(player->getPosition()/BS)<<std::endl;
- // Get player name of this client
- std::wstring name = narrow_to_wide(player->getName());
+ // ActiveObject is added to environment in AsyncRunStep after
+ // the previous addition has been succesfully removed
+}
- // Run script hook
- bool ate = m_script->on_chat_message(player->getName(),
- wide_to_narrow(message));
- // If script ate the message, don't proceed
- if(ate)
- return;
+void Server::handleCommand_Interact(ToServerPacket* pkt)
+{
+ std::string datastring(pkt->getString(0), pkt->getSize());
+ std::istringstream is(datastring, std::ios_base::binary);
- // Line to send to players
- std::wstring line;
- // Whether to send to the player that sent the line
- bool send_to_sender_only = false;
+ /*
+ [0] u16 command
+ [2] u8 action
+ [3] u16 item
+ [5] u32 length of the next item
+ [9] serialized PointedThing
+ actions:
+ 0: start digging (from undersurface) or use
+ 1: stop digging (all parameters ignored)
+ 2: digging completed
+ 3: place block or item (to abovesurface)
+ 4: use item
+ */
+ u8 action = readU8(is);
+ u16 item_i = readU16(is);
+ std::istringstream tmp_is(deSerializeLongString(is), std::ios::binary);
+ PointedThing pointed;
+ pointed.deSerialize(tmp_is);
- // Commands are implemented in Lua, so only catch invalid
- // commands that were not "eaten" and send an error back
- if(message[0] == L'/')
- {
- message = message.substr(1);
- send_to_sender_only = true;
- if(message.length() == 0)
- line += L"-!- Empty command";
- else
- line += L"-!- Invalid command: " + str_split(message, L' ')[0];
- }
- else
- {
- if(checkPriv(player->getName(), "shout")){
- line += L"<";
- line += name;
- line += L"> ";
- line += message;
- } else {
- line += L"-!- You don't have permission to shout.";
- send_to_sender_only = true;
- }
- }
+ verbosestream << "TOSERVER_INTERACT: action=" << (int)action << ", item="
+ << item_i << ", pointed=" << pointed.dump() << std::endl;
- if(line != L"")
- {
- /*
- Send the message to sender
- */
- if (send_to_sender_only)
- {
- SendChatMessage(peer_id, line);
- }
- /*
- Send the message to others
- */
- else
- {
- actionstream<<"CHAT: "<<wide_to_narrow(line)<<std::endl;
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- std::list<u16> clients = m_clients.getClientIDs();
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- for(std::list<u16>::iterator
- i = clients.begin();
- i != clients.end(); ++i)
- {
- if (*i != peer_id)
- SendChatMessage(*i, line);
- }
- }
- }
+ if(player->hp == 0) {
+ verbosestream << "TOSERVER_INTERACT: " << player->getName()
+ << " tried to interact, but is dead!" << std::endl;
+ return;
}
- else if(command == TOSERVER_DAMAGE)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
- u8 damage = readU8(is);
- if(g_settings->getBool("enable_damage"))
- {
- actionstream<<player->getName()<<" damaged by "
- <<(int)damage<<" hp at "<<PP(player->getPosition()/BS)
- <<std::endl;
+ v3f player_pos = playersao->getLastGoodPosition();
- playersao->setHP(playersao->getHP() - damage);
+ // Update wielded item
+ playersao->setWieldIndex(item_i);
- if(playersao->getHP() == 0 && playersao->m_hp_not_sent)
- DiePlayer(peer_id);
+ // Get pointed to node (undefined if not POINTEDTYPE_NODE)
+ v3s16 p_under = pointed.node_undersurface;
+ v3s16 p_above = pointed.node_abovesurface;
- if(playersao->m_hp_not_sent)
- SendPlayerHP(peer_id);
+ // Get pointed to object (NULL if not POINTEDTYPE_OBJECT)
+ ServerActiveObject *pointed_object = NULL;
+ if(pointed.type == POINTEDTHING_OBJECT) {
+ pointed_object = m_env->getActiveObject(pointed.object_id);
+ if(pointed_object == NULL) {
+ verbosestream << "TOSERVER_INTERACT: "
+ "pointed object is NULL" << std::endl;
+ return;
}
+
}
- else if(command == TOSERVER_BREATH)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
- u16 breath = readU16(is);
- playersao->setBreath(breath);
- m_script->player_event(playersao,"breath_changed");
+
+ v3f pointed_pos_under = player_pos;
+ v3f pointed_pos_above = player_pos;
+ if(pointed.type == POINTEDTHING_NODE) {
+ pointed_pos_under = intToFloat(p_under, BS);
+ pointed_pos_above = intToFloat(p_above, BS);
+ }
+ else if(pointed.type == POINTEDTHING_OBJECT) {
+ pointed_pos_under = pointed_object->getBasePosition();
+ pointed_pos_above = pointed_pos_under;
}
- else if(command == TOSERVER_PASSWORD)
- {
- /*
- [0] u16 TOSERVER_PASSWORD
- [2] u8[28] old password
- [30] u8[28] new password
- */
- if(datasize != 2+PASSWORD_SIZE*2)
+ /*
+ Check that target is reasonably close
+ (only when digging or placing things)
+ */
+ if(action == 0 || action == 2 || action == 3) {
+ float d = player_pos.getDistanceFrom(pointed_pos_under);
+ float max_d = BS * 14; // Just some large enough value
+ if(d > max_d) {
+ actionstream << "Player " << player->getName()
+ << " tried to access " << pointed.dump()
+ << " from too far: "
+ << "d=" << d <<", max_d=" << max_d
+ << ". ignoring." << std::endl;
+ // Re-send block to revert change on client-side
+ RemoteClient *client = getClient(pkt->getPeerId());
+ v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+ client->SetBlockNotSent(blockpos);
+ // Call callbacks
+ m_script->on_cheat(playersao, "interacted_too_far");
+ // Do nothing else
return;
- /*char password[PASSWORD_SIZE];
- for(u32 i=0; i<PASSWORD_SIZE-1; i++)
- password[i] = data[2+i];
- password[PASSWORD_SIZE-1] = 0;*/
- std::string oldpwd;
- for(u32 i=0; i<PASSWORD_SIZE-1; i++)
- {
- char c = data[2+i];
- if(c == 0)
- break;
- oldpwd += c;
- }
- std::string newpwd;
- for(u32 i=0; i<PASSWORD_SIZE-1; i++)
- {
- char c = data[2+PASSWORD_SIZE+i];
- if(c == 0)
- break;
- newpwd += c;
}
+ }
- if(!base64_is_valid(newpwd)){
- infostream<<"Server: "<<player->getName()<<" supplied invalid password hash"<<std::endl;
- // Wrong old password supplied!!
- SendChatMessage(peer_id, L"Invalid new password hash supplied. Password NOT changed.");
- return;
+ /*
+ Make sure the player is allowed to do it
+ */
+ if(!checkPriv(player->getName(), "interact")) {
+ actionstream<<player->getName()<<" attempted to interact with "
+ <<pointed.dump()<<" without 'interact' privilege"
+ <<std::endl;
+ // Re-send block to revert change on client-side
+ RemoteClient *client = getClient(pkt->getPeerId());
+ // Digging completed -> under
+ if(action == 2) {
+ v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+ client->SetBlockNotSent(blockpos);
+ }
+ // Placement -> above
+ if(action == 3) {
+ v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
+ client->SetBlockNotSent(blockpos);
}
+ return;
+ }
- infostream<<"Server: Client requests a password change from "
- <<"'"<<oldpwd<<"' to '"<<newpwd<<"'"<<std::endl;
+ /*
+ If something goes wrong, this player is to blame
+ */
+ RollbackScopeActor rollback_scope(m_rollback,
+ std::string("player:")+player->getName());
- std::string playername = player->getName();
+ /*
+ 0: start digging or punch object
+ */
+ if(action == 0) {
+ if(pointed.type == POINTEDTHING_NODE) {
+ /*
+ NOTE: This can be used in the future to check if
+ somebody is cheating, by checking the timing.
+ */
+ MapNode n(CONTENT_IGNORE);
+ bool pos_ok;
+ n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
+ if (pos_ok)
+ n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
- std::string checkpwd;
- m_script->getAuth(playername, &checkpwd, NULL);
+ if (!pos_ok) {
+ infostream << "Server: Not punching: Node not found."
+ << " Adding block to emerge queue."
+ << std::endl;
+ m_emerge->enqueueBlockEmerge(pkt->getPeerId(), getNodeBlockPos(p_above), false);
+ }
- if(oldpwd != checkpwd)
- {
- infostream<<"Server: invalid old password"<<std::endl;
- // Wrong old password supplied!!
- SendChatMessage(peer_id, L"Invalid old password supplied. Password NOT changed.");
- return;
+ if(n.getContent() != CONTENT_IGNORE)
+ m_script->node_on_punch(p_under, n, playersao, pointed);
+ // Cheat prevention
+ playersao->noCheatDigStart(p_under);
}
+ else if(pointed.type == POINTEDTHING_OBJECT) {
+ // Skip if object has been removed
+ if(pointed_object->m_removed)
+ return;
- bool success = m_script->setPassword(playername, newpwd);
- if(success){
- actionstream<<player->getName()<<" changes password"<<std::endl;
- SendChatMessage(peer_id, L"Password change successful.");
- } else {
- actionstream<<player->getName()<<" tries to change password but "
- <<"it fails"<<std::endl;
- SendChatMessage(peer_id, L"Password change failed or inavailable.");
+ actionstream<<player->getName()<<" punches object "
+ <<pointed.object_id<<": "
+ <<pointed_object->getDescription()<<std::endl;
+
+ ItemStack punchitem = playersao->getWieldedItem();
+ ToolCapabilities toolcap =
+ punchitem.getToolCapabilities(m_itemdef);
+ v3f dir = (pointed_object->getBasePosition() -
+ (player->getPosition() + player->getEyeOffset())
+ ).normalize();
+ float time_from_last_punch =
+ playersao->resetTimeFromLastPunch();
+ pointed_object->punch(dir, &toolcap, playersao,
+ time_from_last_punch);
}
- }
- else if(command == TOSERVER_PLAYERITEM)
- {
- if (datasize < 2+2)
- return;
- u16 item = readU16(&data[2]);
- playersao->setWieldIndex(item);
- }
- else if(command == TOSERVER_RESPAWN)
- {
- if(player->hp != 0 || !g_settings->getBool("enable_damage"))
- return;
+ } // action == 0
- RespawnPlayer(peer_id);
+ /*
+ 1: stop digging
+ */
+ else if(action == 1) {
+ } // action == 1
- actionstream<<player->getName()<<" respawns at "
- <<PP(player->getPosition()/BS)<<std::endl;
+ /*
+ 2: Digging completed
+ */
+ else if(action == 2) {
+ // Only digging of nodes
+ if(pointed.type == POINTEDTHING_NODE) {
+ bool pos_ok;
+ MapNode n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
+ if (!pos_ok) {
+ infostream << "Server: Not finishing digging: Node not found."
+ << " Adding block to emerge queue."
+ << std::endl;
+ m_emerge->enqueueBlockEmerge(pkt->getPeerId(), getNodeBlockPos(p_above), false);
+ }
- // ActiveObject is added to environment in AsyncRunStep after
- // the previous addition has been succesfully removed
- }
- else if(command == TOSERVER_INTERACT)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
+ /* Cheat prevention */
+ bool is_valid_dig = true;
+ if(!isSingleplayer() && !g_settings->getBool("disable_anticheat")) {
+ v3s16 nocheat_p = playersao->getNoCheatDigPos();
+ float nocheat_t = playersao->getNoCheatDigTime();
+ playersao->noCheatDigEnd();
+ // If player didn't start digging this, ignore dig
+ if(nocheat_p != p_under) {
+ infostream << "Server: NoCheat: " << player->getName()
+ << " started digging "
+ << PP(nocheat_p) << " and completed digging "
+ << PP(p_under) << "; not digging." << std::endl;
+ is_valid_dig = false;
+ // Call callbacks
+ m_script->on_cheat(playersao, "finished_unknown_dig");
+ }
+ // Get player's wielded item
+ ItemStack playeritem;
+ InventoryList *mlist = playersao->getInventory()->getList("main");
+ if(mlist != NULL)
+ playeritem = mlist->getItem(playersao->getWieldIndex());
+ ToolCapabilities playeritem_toolcap =
+ playeritem.getToolCapabilities(m_itemdef);
+ // Get diggability and expected digging time
+ DigParams params = getDigParams(m_nodedef->get(n).groups,
+ &playeritem_toolcap);
+ // If can't dig, try hand
+ if(!params.diggable) {
+ const ItemDefinition &hand = m_itemdef->get("");
+ const ToolCapabilities *tp = hand.tool_capabilities;
+ if(tp)
+ params = getDigParams(m_nodedef->get(n).groups, tp);
+ }
+ // If can't dig, ignore dig
+ if(!params.diggable) {
+ infostream << "Server: NoCheat: " << player->getName()
+ << " completed digging " << PP(p_under)
+ << ", which is not diggable with tool. not digging."
+ << std::endl;
+ is_valid_dig = false;
+ // Call callbacks
+ m_script->on_cheat(playersao, "dug_unbreakable");
+ }
+ // Check digging time
+ // If already invalidated, we don't have to
+ if(!is_valid_dig) {
+ // Well not our problem then
+ }
+ // Clean and long dig
+ else if(params.time > 2.0 && nocheat_t * 1.2 > params.time) {
+ // All is good, but grab time from pool; don't care if
+ // it's actually available
+ playersao->getDigPool().grab(params.time);
+ }
+ // Short or laggy dig
+ // Try getting the time from pool
+ else if(playersao->getDigPool().grab(params.time)) {
+ // All is good
+ }
+ // Dig not possible
+ else {
+ infostream << "Server: NoCheat: " << player->getName()
+ << " completed digging " << PP(p_under)
+ << "too fast; not digging." << std::endl;
+ is_valid_dig = false;
+ // Call callbacks
+ m_script->on_cheat(playersao, "dug_too_fast");
+ }
+ }
- /*
- [0] u16 command
- [2] u8 action
- [3] u16 item
- [5] u32 length of the next item
- [9] serialized PointedThing
- actions:
- 0: start digging (from undersurface) or use
- 1: stop digging (all parameters ignored)
- 2: digging completed
- 3: place block or item (to abovesurface)
- 4: use item
- */
- u8 action = readU8(is);
- u16 item_i = readU16(is);
- std::istringstream tmp_is(deSerializeLongString(is), std::ios::binary);
- PointedThing pointed;
- pointed.deSerialize(tmp_is);
+ /* Actually dig node */
- verbosestream<<"TOSERVER_INTERACT: action="<<(int)action<<", item="
- <<item_i<<", pointed="<<pointed.dump()<<std::endl;
+ if(is_valid_dig && n.getContent() != CONTENT_IGNORE)
+ m_script->node_on_dig(p_under, n, playersao);
- if(player->hp == 0)
- {
- verbosestream<<"TOSERVER_INTERACT: "<<player->getName()
- <<" tried to interact, but is dead!"<<std::endl;
- return;
+ v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+ RemoteClient *client = getClient(pkt->getPeerId());
+ // Send unusual result (that is, node not being removed)
+ if(m_env->getMap().getNodeNoEx(p_under).getContent() != CONTENT_AIR) {
+ // Re-send block to revert change on client-side
+ client->SetBlockNotSent(blockpos);
+ }
+ else {
+ client->ResendBlockIfOnWire(blockpos);
+ }
}
+ } // action == 2
- v3f player_pos = playersao->getLastGoodPosition();
+ /*
+ 3: place block or right-click object
+ */
+ else if(action == 3) {
+ ItemStack item = playersao->getWieldedItem();
- // Update wielded item
- playersao->setWieldIndex(item_i);
+ // Reset build time counter
+ if(pointed.type == POINTEDTHING_NODE &&
+ item.getDefinition(m_itemdef).type == ITEM_NODE)
+ getClient(pkt->getPeerId())->m_time_from_building = 0.0;
- // Get pointed to node (undefined if not POINTEDTYPE_NODE)
- v3s16 p_under = pointed.node_undersurface;
- v3s16 p_above = pointed.node_abovesurface;
+ if(pointed.type == POINTEDTHING_OBJECT) {
+ // Right click object
- // Get pointed to object (NULL if not POINTEDTYPE_OBJECT)
- ServerActiveObject *pointed_object = NULL;
- if(pointed.type == POINTEDTHING_OBJECT)
- {
- pointed_object = m_env->getActiveObject(pointed.object_id);
- if(pointed_object == NULL)
- {
- verbosestream<<"TOSERVER_INTERACT: "
- "pointed object is NULL"<<std::endl;
+ // Skip if object has been removed
+ if(pointed_object->m_removed)
return;
- }
- }
+ actionstream << player->getName() << " right-clicks object "
+ << pointed.object_id << ": "
+ << pointed_object->getDescription() << std::endl;
- v3f pointed_pos_under = player_pos;
- v3f pointed_pos_above = player_pos;
- if(pointed.type == POINTEDTHING_NODE)
- {
- pointed_pos_under = intToFloat(p_under, BS);
- pointed_pos_above = intToFloat(p_above, BS);
- }
- else if(pointed.type == POINTEDTHING_OBJECT)
- {
- pointed_pos_under = pointed_object->getBasePosition();
- pointed_pos_above = pointed_pos_under;
+ // Do stuff
+ pointed_object->rightClick(playersao);
}
+ else if(m_script->item_OnPlace(
+ item, playersao, pointed)) {
+ // Placement was handled in lua
- /*
- Check that target is reasonably close
- (only when digging or placing things)
- */
- if(action == 0 || action == 2 || action == 3)
- {
- float d = player_pos.getDistanceFrom(pointed_pos_under);
- float max_d = BS * 14; // Just some large enough value
- if(d > max_d){
- actionstream<<"Player "<<player->getName()
- <<" tried to access "<<pointed.dump()
- <<" from too far: "
- <<"d="<<d<<", max_d="<<max_d
- <<". ignoring."<<std::endl;
- // Re-send block to revert change on client-side
- RemoteClient *client = getClient(peer_id);
- v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
- client->SetBlockNotSent(blockpos);
- // Call callbacks
- m_script->on_cheat(playersao, "interacted_too_far");
- // Do nothing else
- return;
- }
+ // Apply returned ItemStack
+ playersao->setWieldedItem(item);
}
- /*
- Make sure the player is allowed to do it
- */
- if(!checkPriv(player->getName(), "interact"))
- {
- actionstream<<player->getName()<<" attempted to interact with "
- <<pointed.dump()<<" without 'interact' privilege"
- <<std::endl;
- // Re-send block to revert change on client-side
- RemoteClient *client = getClient(peer_id);
- // Digging completed -> under
- if(action == 2){
- v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
- client->SetBlockNotSent(blockpos);
+ // If item has node placement prediction, always send the
+ // blocks to make sure the client knows what exactly happened
+ RemoteClient *client = getClient(pkt->getPeerId());
+ v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
+ v3s16 blockpos2 = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+ if(item.getDefinition(m_itemdef).node_placement_prediction != "") {
+ client->SetBlockNotSent(blockpos);
+ if(blockpos2 != blockpos) {
+ client->SetBlockNotSent(blockpos2);
}
- // Placement -> above
- if(action == 3){
- v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
- client->SetBlockNotSent(blockpos);
+ }
+ else {
+ client->ResendBlockIfOnWire(blockpos);
+ if(blockpos2 != blockpos) {
+ client->ResendBlockIfOnWire(blockpos2);
}
- return;
}
+ } // action == 3
- /*
- If something goes wrong, this player is to blame
- */
- RollbackScopeActor rollback_scope(m_rollback,
- std::string("player:")+player->getName());
+ /*
+ 4: use
+ */
+ else if(action == 4) {
+ ItemStack item = playersao->getWieldedItem();
- /*
- 0: start digging or punch object
- */
- if(action == 0)
- {
- if(pointed.type == POINTEDTHING_NODE)
- {
- /*
- NOTE: This can be used in the future to check if
- somebody is cheating, by checking the timing.
- */
- MapNode n(CONTENT_IGNORE);
- bool pos_ok;
- n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
- if (pos_ok)
- n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
-
- if (!pos_ok) {
- infostream<<"Server: Not punching: Node not found."
- <<" Adding block to emerge queue."
- <<std::endl;
- m_emerge->enqueueBlockEmerge(peer_id, getNodeBlockPos(p_above), false);
- }
+ actionstream << player->getName() << " uses " << item.name
+ << ", pointing at " << pointed.dump() << std::endl;
- if(n.getContent() != CONTENT_IGNORE)
- m_script->node_on_punch(p_under, n, playersao, pointed);
- // Cheat prevention
- playersao->noCheatDigStart(p_under);
- }
- else if(pointed.type == POINTEDTHING_OBJECT)
- {
- // Skip if object has been removed
- if(pointed_object->m_removed)
- return;
-
- actionstream<<player->getName()<<" punches object "
- <<pointed.object_id<<": "
- <<pointed_object->getDescription()<<std::endl;
-
- ItemStack punchitem = playersao->getWieldedItem();
- ToolCapabilities toolcap =
- punchitem.getToolCapabilities(m_itemdef);
- v3f dir = (pointed_object->getBasePosition() -
- (player->getPosition() + player->getEyeOffset())
- ).normalize();
- float time_from_last_punch =
- playersao->resetTimeFromLastPunch();
- pointed_object->punch(dir, &toolcap, playersao,
- time_from_last_punch);
- }
+ if(m_script->item_OnUse(
+ item, playersao, pointed)) {
+ // Apply returned ItemStack
+ playersao->setWieldedItem(item);
+ }
- } // action == 0
+ } // action == 4
- /*
- 1: stop digging
- */
- else if(action == 1)
- {
- } // action == 1
- /*
- 2: Digging completed
- */
- else if(action == 2)
- {
- // Only digging of nodes
- if(pointed.type == POINTEDTHING_NODE)
- {
- bool pos_ok;
- MapNode n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
- if (!pos_ok) {
- infostream << "Server: Not finishing digging: Node not found."
- << " Adding block to emerge queue."
- << std::endl;
- m_emerge->enqueueBlockEmerge(peer_id, getNodeBlockPos(p_above), false);
- }
+ /*
+ Catch invalid actions
+ */
+ else {
+ infostream << "WARNING: Server: Invalid action "
+ << action << std::endl;
+ }
+}
- /* Cheat prevention */
- bool is_valid_dig = true;
- if(!isSingleplayer() && !g_settings->getBool("disable_anticheat"))
- {
- v3s16 nocheat_p = playersao->getNoCheatDigPos();
- float nocheat_t = playersao->getNoCheatDigTime();
- playersao->noCheatDigEnd();
- // If player didn't start digging this, ignore dig
- if(nocheat_p != p_under){
- infostream<<"Server: NoCheat: "<<player->getName()
- <<" started digging "
- <<PP(nocheat_p)<<" and completed digging "
- <<PP(p_under)<<"; not digging."<<std::endl;
- is_valid_dig = false;
- // Call callbacks
- m_script->on_cheat(playersao, "finished_unknown_dig");
- }
- // Get player's wielded item
- ItemStack playeritem;
- InventoryList *mlist = playersao->getInventory()->getList("main");
- if(mlist != NULL)
- playeritem = mlist->getItem(playersao->getWieldIndex());
- ToolCapabilities playeritem_toolcap =
- playeritem.getToolCapabilities(m_itemdef);
- // Get diggability and expected digging time
- DigParams params = getDigParams(m_nodedef->get(n).groups,
- &playeritem_toolcap);
- // If can't dig, try hand
- if(!params.diggable){
- const ItemDefinition &hand = m_itemdef->get("");
- const ToolCapabilities *tp = hand.tool_capabilities;
- if(tp)
- params = getDigParams(m_nodedef->get(n).groups, tp);
- }
- // If can't dig, ignore dig
- if(!params.diggable){
- infostream<<"Server: NoCheat: "<<player->getName()
- <<" completed digging "<<PP(p_under)
- <<", which is not diggable with tool. not digging."
- <<std::endl;
- is_valid_dig = false;
- // Call callbacks
- m_script->on_cheat(playersao, "dug_unbreakable");
- }
- // Check digging time
- // If already invalidated, we don't have to
- if(!is_valid_dig){
- // Well not our problem then
- }
- // Clean and long dig
- else if(params.time > 2.0 && nocheat_t * 1.2 > params.time){
- // All is good, but grab time from pool; don't care if
- // it's actually available
- playersao->getDigPool().grab(params.time);
- }
- // Short or laggy dig
- // Try getting the time from pool
- else if(playersao->getDigPool().grab(params.time)){
- // All is good
- }
- // Dig not possible
- else{
- infostream<<"Server: NoCheat: "<<player->getName()
- <<" completed digging "<<PP(p_under)
- <<"too fast; not digging."<<std::endl;
- is_valid_dig = false;
- // Call callbacks
- m_script->on_cheat(playersao, "dug_too_fast");
- }
- }
+void Server::handleCommand_RemovedSounds(ToServerPacket* pkt)
+{
+ u16 num;
+ *pkt >> num;
+ for(int k=0; k<num; k++) {
+ s32 id;
- /* Actually dig node */
+ *pkt >> id;
- if(is_valid_dig && n.getContent() != CONTENT_IGNORE)
- m_script->node_on_dig(p_under, n, playersao);
+ std::map<s32, ServerPlayingSound>::iterator i =
+ m_playing_sounds.find(id);
- v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
- RemoteClient *client = getClient(peer_id);
- // Send unusual result (that is, node not being removed)
- if(m_env->getMap().getNodeNoEx(p_under).getContent() != CONTENT_AIR)
- {
- // Re-send block to revert change on client-side
- client->SetBlockNotSent(blockpos);
- }
- else {
- client->ResendBlockIfOnWire(blockpos);
- }
- }
- } // action == 2
+ if(i == m_playing_sounds.end())
+ continue;
- /*
- 3: place block or right-click object
- */
- else if(action == 3)
- {
- ItemStack item = playersao->getWieldedItem();
+ ServerPlayingSound &psound = i->second;
+ psound.clients.erase(pkt->getPeerId());
+ if(psound.clients.empty())
+ m_playing_sounds.erase(i++);
+ }
+}
- // Reset build time counter
- if(pointed.type == POINTEDTHING_NODE &&
- item.getDefinition(m_itemdef).type == ITEM_NODE)
- getClient(peer_id)->m_time_from_building = 0.0;
+void Server::handleCommand_NodeMetaFields(ToServerPacket* pkt)
+{
+ v3s16 p;
+ std::string formname;
+ u16 num;
- if(pointed.type == POINTEDTHING_OBJECT)
- {
- // Right click object
+ *pkt >> p >> formname >> num;
- // Skip if object has been removed
- if(pointed_object->m_removed)
- return;
+ std::map<std::string, std::string> fields;
+ for(int k=0; k<num; k++) {
+ std::string fieldname;
+ *pkt >> fieldname;
+ fields[fieldname] = pkt->readLongString();
+ }
- actionstream<<player->getName()<<" right-clicks object "
- <<pointed.object_id<<": "
- <<pointed_object->getDescription()<<std::endl;
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- // Do stuff
- pointed_object->rightClick(playersao);
- }
- else if(m_script->item_OnPlace(
- item, playersao, pointed))
- {
- // Placement was handled in lua
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
- // Apply returned ItemStack
- playersao->setWieldedItem(item);
- }
+ // If something goes wrong, this player is to blame
+ RollbackScopeActor rollback_scope(m_rollback,
+ std::string("player:")+player->getName());
- // If item has node placement prediction, always send the
- // blocks to make sure the client knows what exactly happened
- RemoteClient *client = getClient(peer_id);
- v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
- v3s16 blockpos2 = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
- if(item.getDefinition(m_itemdef).node_placement_prediction != "") {
- client->SetBlockNotSent(blockpos);
- if(blockpos2 != blockpos) {
- client->SetBlockNotSent(blockpos2);
- }
- }
- else {
- client->ResendBlockIfOnWire(blockpos);
- if(blockpos2 != blockpos) {
- client->ResendBlockIfOnWire(blockpos2);
- }
- }
- } // action == 3
+ // Check the target node for rollback data; leave others unnoticed
+ RollbackNode rn_old(&m_env->getMap(), p, this);
- /*
- 4: use
- */
- else if(action == 4)
- {
- ItemStack item = playersao->getWieldedItem();
+ m_script->node_on_receive_fields(p, formname, fields, playersao);
- actionstream<<player->getName()<<" uses "<<item.name
- <<", pointing at "<<pointed.dump()<<std::endl;
+ // Report rollback data
+ RollbackNode rn_new(&m_env->getMap(), p, this);
+ if(rollback() && rn_new != rn_old){
+ RollbackAction action;
+ action.setSetNode(p, rn_old, rn_new);
+ rollback()->reportAction(action);
+ }
+}
- if(m_script->item_OnUse(
- item, playersao, pointed))
- {
- // Apply returned ItemStack
- playersao->setWieldedItem(item);
- }
+void Server::handleCommand_InventoryFields(ToServerPacket* pkt)
+{
+ std::string formname;
+ u16 num;
- } // action == 4
+ *pkt >> formname >> num;
+ std::map<std::string, std::string> fields;
+ for(int k=0; k<num; k++) {
+ std::string fieldname;
+ *pkt >> fieldname;
+ fields[fieldname] = pkt->readLongString();
+ }
- /*
- Catch invalid actions
- */
- else
- {
- infostream<<"WARNING: Server: Invalid action "
- <<action<<std::endl;
- }
+ Player *player = m_env->getPlayer(pkt->getPeerId());
+ if(player == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
}
- else if(command == TOSERVER_REMOVED_SOUNDS)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
-
- int num = readU16(is);
- for(int k=0; k<num; k++){
- s32 id = readS32(is);
- std::map<s32, ServerPlayingSound>::iterator i =
- m_playing_sounds.find(id);
- if(i == m_playing_sounds.end())
- continue;
- ServerPlayingSound &psound = i->second;
- psound.clients.erase(peer_id);
- if(psound.clients.empty())
- m_playing_sounds.erase(i++);
+
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if(playersao == NULL) {
+ errorstream << "Server::ProcessData(): Cancelling: "
+ "No player object for peer_id=" << pkt->getPeerId()
+ << " disconnecting peer!" << std::endl;
+ m_con.DisconnectPeer(pkt->getPeerId());
+ return;
+ }
+
+ m_script->on_playerReceiveFields(playersao, formname, fields);
+}
+
+inline void Server::handleCommand(ToServerPacket* pkt)
+{
+ const ToServerCommandHandler& opHandle = toServerCommandTable[pkt->getCommand()];
+ (this->*opHandle.handler)(pkt);
+}
+
+void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id)
+{
+ DSTACK(__FUNCTION_NAME);
+ // Environment is locked first.
+ JMutexAutoLock envlock(m_env_mutex);
+
+ ScopeProfiler sp(g_profiler, "Server::ProcessData");
+
+ try {
+ Address address = getPeerAddress(peer_id);
+ std::string addr_s = address.serializeString();
+
+ if(m_banmanager->isIpBanned(addr_s)) {
+ std::string ban_name = m_banmanager->getBanName(addr_s);
+ infostream << "Server: A banned client tried to connect from "
+ << addr_s << "; banned name was "
+ << ban_name << std::endl;
+ // This actually doesn't seem to transfer to the client
+ DenyAccess(peer_id, L"Your ip is banned. Banned name was "
+ + narrow_to_wide(ban_name));
+ return;
}
}
- else if(command == TOSERVER_NODEMETA_FIELDS)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
-
- v3s16 p = readV3S16(is);
- std::string formname = deSerializeString(is);
- int num = readU16(is);
- std::map<std::string, std::string> fields;
- for(int k=0; k<num; k++){
- std::string fieldname = deSerializeString(is);
- std::string fieldvalue = deSerializeLongString(is);
- fields[fieldname] = fieldvalue;
+ catch(con::PeerNotFoundException &e) {
+ /*
+ * no peer for this packet found
+ * most common reason is peer timeout, e.g. peer didn't
+ * respond for some time, your server was overloaded or
+ * things like that.
+ */
+ infostream << "Server::ProcessData(): Cancelling: peer "
+ << peer_id << " not found" << std::endl;
+ return;
+ }
+
+ try {
+ if(datasize < 2)
+ return;
+
+ ToServerPacket* pkt = new ToServerPacket(data, datasize, peer_id);
+
+ ToServerCommand command = pkt->getCommand();
+
+ // Command must be handled into ToServerCommandHandler
+ if (command >= TOSERVER_NUM_MSG_TYPES) {
+ infostream << "Server: Ignoring unknown command "
+ << command << std::endl;
}
- // If something goes wrong, this player is to blame
- RollbackScopeActor rollback_scope(m_rollback,
- std::string("player:")+player->getName());
+ if (toServerCommandTable[command].state == TOSERVER_STATE_NOT_CONNECTED) {
+ handleCommand(pkt);
+ delete pkt;
+ return;
+ }
- // Check the target node for rollback data; leave others unnoticed
- RollbackNode rn_old(&m_env->getMap(), p, this);
+ u8 peer_ser_ver = getClient(peer_id, CS_InitDone)->serialization_version;
- m_script->node_on_receive_fields(p, formname, fields,playersao);
+ if(peer_ser_ver == SER_FMT_VER_INVALID) {
+ errorstream << "Server::ProcessData(): Cancelling: Peer"
+ " serialization format invalid or not initialized."
+ " Skipping incoming command=" << command << std::endl;
- // Report rollback data
- RollbackNode rn_new(&m_env->getMap(), p, this);
- if(rollback() && rn_new != rn_old){
- RollbackAction action;
- action.setSetNode(p, rn_old, rn_new);
- rollback()->reportAction(action);
+ delete pkt;
+ return;
}
- }
- else if(command == TOSERVER_INVENTORY_FIELDS)
- {
- std::string datastring((char*)&data[2], datasize-2);
- std::istringstream is(datastring, std::ios_base::binary);
-
- std::string formname = deSerializeString(is);
- int num = readU16(is);
- std::map<std::string, std::string> fields;
- for(int k=0; k<num; k++){
- std::string fieldname = deSerializeString(is);
- std::string fieldvalue = deSerializeLongString(is);
- fields[fieldname] = fieldvalue;
+
+ /* Handle commands related to client startup */
+ if (toServerCommandTable[command].state == TOSERVER_STATE_STARTUP) {
+ handleCommand(pkt);
+ delete pkt;
+ return;
}
- m_script->on_playerReceiveFields(playersao, formname, fields);
- }
- else
- {
- infostream<<"Server::ProcessData(): Ignoring "
- "unknown command "<<command<<std::endl;
- }
+ if (m_clients.getClientState(peer_id) < CS_Active) {
+ if (command == TOSERVER_PLAYERPOS) return;
+
+ errorstream << "Got packet command: " << command << " for peer id "
+ << peer_id << " but client isn't active yet. Dropping packet "
+ << std::endl;
+
+ delete pkt;
+ return;
+ }
+
+ handleCommand(pkt);
+ delete pkt;
- } //try
- catch(SendFailedException &e)
- {
- errorstream<<"Server::ProcessData(): SendFailedException: "
- <<"what="<<e.what()
- <<std::endl;
+ }
+ catch(SendFailedException &e) {
+ errorstream << "Server::ProcessData(): SendFailedException: "
+ << "what=" << e.what()
+ << std::endl;
}
}
@@ -2753,7 +2886,6 @@ void Server::setTimeOfDay(u32 time)
void Server::onMapEditEvent(MapEditEvent *event)
{
- //infostream<<"Server::onMapEditEvent()"<<std::endl;
if(m_ignore_map_edit_events)
return;
if(m_ignore_map_edit_events_area.contains(event->getArea()))
@@ -2764,13 +2896,10 @@ void Server::onMapEditEvent(MapEditEvent *event)
Inventory* Server::getInventory(const InventoryLocation &loc)
{
- switch(loc.type){
+ switch (loc.type) {
case InventoryLocation::UNDEFINED:
- {}
- break;
case InventoryLocation::CURRENT_PLAYER:
- {}
- break;
+ break;
case InventoryLocation::PLAYER:
{
Player *player = m_env->getPlayer(loc.name.c_str());
@@ -2781,7 +2910,7 @@ Inventory* Server::getInventory(const InventoryLocation &loc)
return NULL;
return playersao->getInventory();
}
- break;
+ break;
case InventoryLocation::NODEMETA:
{
NodeMetadata *meta = m_env->getMap().getNodeMetadata(loc.p);
@@ -2789,14 +2918,14 @@ Inventory* Server::getInventory(const InventoryLocation &loc)
return NULL;
return meta->getInventory();
}
- break;
+ break;
case InventoryLocation::DETACHED:
{
if(m_detached_inventories.count(loc.name) == 0)
return NULL;
return m_detached_inventories[loc.name];
}
- break;
+ break;
default:
assert(0);
}
@@ -2806,8 +2935,7 @@ void Server::setInventoryModified(const InventoryLocation &loc)
{
switch(loc.type){
case InventoryLocation::UNDEFINED:
- {}
- break;
+ break;
case InventoryLocation::PLAYER:
{
Player *player = m_env->getPlayer(loc.name.c_str());
@@ -2819,7 +2947,7 @@ void Server::setInventoryModified(const InventoryLocation &loc)
playersao->m_inventory_not_sent = true;
playersao->m_wielded_item_not_sent = true;
}
- break;
+ break;
case InventoryLocation::NODEMETA:
{
v3s16 blockpos = getNodeBlockPos(loc.p);
@@ -2830,12 +2958,12 @@ void Server::setInventoryModified(const InventoryLocation &loc)
setBlockNotSent(blockpos);
}
- break;
+ break;
case InventoryLocation::DETACHED:
{
sendDetachedInventory(loc.name,PEER_ID_INEXISTENT);
}
- break;
+ break;
default:
assert(0);
}
diff --git a/src/server.h b/src/server.h
index 3d6b00d99..a61b70ec0 100644
--- a/src/server.h
+++ b/src/server.h
@@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/thread.h"
#include "environment.h"
#include "clientiface.h"
+#include "network/toserverpacket.h"
#include <string>
#include <list>
#include <map>
@@ -187,6 +188,35 @@ public:
void AsyncRunStep(bool initial_step=false);
void Receive();
PlayerSAO* StageTwoClientInit(u16 peer_id);
+
+ /*
+ * Command Handlers
+ */
+
+ void handleCommand(ToServerPacket* pkt);
+
+ void handleCommand_Null(ToServerPacket* pkt) {};
+ void handleCommand_Deprecated(ToServerPacket* pkt);
+ void handleCommand_Init(ToServerPacket* pkt);
+ void handleCommand_Init2(ToServerPacket* pkt);
+ void handleCommand_RequestMedia(ToServerPacket* pkt);
+ void handleCommand_ReceivedMedia(ToServerPacket* pkt);
+ void handleCommand_ClientReady(ToServerPacket* pkt);
+ void handleCommand_GotBlocks(ToServerPacket* pkt);
+ void handleCommand_PlayerPos(ToServerPacket* pkt);
+ void handleCommand_DeletedBlocks(ToServerPacket* pkt);
+ void handleCommand_InventoryAction(ToServerPacket* pkt);
+ void handleCommand_ChatMessage(ToServerPacket* pkt);
+ void handleCommand_Damage(ToServerPacket* pkt);
+ void handleCommand_Breath(ToServerPacket* pkt);
+ void handleCommand_Password(ToServerPacket* pkt);
+ void handleCommand_PlayerItem(ToServerPacket* pkt);
+ void handleCommand_Respawn(ToServerPacket* pkt);
+ void handleCommand_Interact(ToServerPacket* pkt);
+ void handleCommand_RemovedSounds(ToServerPacket* pkt);
+ void handleCommand_NodeMetaFields(ToServerPacket* pkt);
+ void handleCommand_InventoryFields(ToServerPacket* pkt);
+
void ProcessData(u8 *data, u32 datasize, u16 peer_id);
// Environment must be locked when called
@@ -309,7 +339,7 @@ public:
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
Map & getMap() { return m_env->getMap(); }
ServerEnvironment & getEnv() { return *m_env; }
-
+
u32 hudAdd(Player *player, HudElement *element);
bool hudRemove(Player *player, u32 id);
bool hudChange(Player *player, u32 id, HudElementStat stat, void *value);
@@ -320,13 +350,13 @@ public:
inline Address getPeerAddress(u16 peer_id)
{ return m_con.GetPeerAddress(peer_id); }
-
+
bool setLocalPlayerAnimations(Player *player, v2s32 animation_frames[4], f32 frame_speed);
bool setPlayerEyeOffset(Player *player, v3f first, v3f third);
bool setSky(Player *player, const video::SColor &bgcolor,
const std::string &type, const std::vector<std::string> &params);
-
+
bool overrideDayNightRatio(Player *player, bool do_override,
float brightness);
@@ -379,7 +409,7 @@ private:
void SendSetSky(u16 peer_id, const video::SColor &bgcolor,
const std::string &type, const std::vector<std::string> &params);
void SendOverrideDayNightRatio(u16 peer_id, bool do_override, float ratio);
-
+
/*
Send a node removal/addition event to all clients except ignore_id.
Additionally, if far_players!=NULL, players further away than
diff --git a/src/test.cpp b/src/test.cpp
index 80494e07a..b4158f241 100644
--- a/src/test.cpp
+++ b/src/test.cpp
@@ -42,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/numeric.h"
#include "util/serialize.h"
#include "noise.h" // PseudoRandom used for random data for compression
-#include "clientserver.h" // LATEST_PROTOCOL_VERSION
+#include "network/networkprotocol.h" // LATEST_PROTOCOL_VERSION
#include <algorithm>
/*