From edc56513950cca37593b01c9b955fee9f0aec32a Mon Sep 17 00:00:00 2001 From: chacha Date: Thu, 20 Apr 2023 14:46:12 +0200 Subject: [PATCH] first commit --- README.md | 61 +++- Run.sh | 114 ++++++++ Sources/Classes/ChaChaRESTStats.uc | 436 +++++++++++++++++++++++++++++ Sources/make.bat | 2 + System/ChaChaRESTStats.u | Bin 0 -> 21872 bytes 5 files changed, 610 insertions(+), 3 deletions(-) create mode 100644 Run.sh create mode 100644 Sources/Classes/ChaChaRESTStats.uc create mode 100644 Sources/make.bat create mode 100644 System/ChaChaRESTStats.u diff --git a/README.md b/README.md index 11630ef..ca05943 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,60 @@ # UT99-Mod-ChaChaRESTStats -A tiny mod to allow reading server stats with REST requests. - -/!\ Require UT99 v451+, not compatible with v436, prefered 469+. \ No newline at end of file +A tiny mod to allow reading server stats with REST requests. + +!!! Require UT99 v451+, not compatible with v436, prefered 469+. + +# Installation +## On Linux (crudini required) + + ./Run.sh () + +## Manual installation + +- Copy __ChaChaRESTStats.u__ file to your UT99 __System__ dir + +- Edit UnrealTournament.ini and update / adapt [UWeb.WebServer] section: + + + [UWeb.WebServer] + Applications[0]=UTServerAdmin.UTServerAdmin + ApplicationPaths[0]=/ServerAdmin + Applications[1]=UTServerAdmin.UTImageServer + ApplicationPaths[1]=/images + Applications[2]=ChaChaRESTStats.ChaChaRESTStats + ApplicationPaths[2]=/api/v1 + DefaultApplication=0 + bEnabled=True + ListenPort= + +## Available Resources: +__GET__ /api/v1/map_list + +__GET__ /api/v1/current_all + +__GET__ /api/v1/current_game + +__GET__ /api/v1/current_players + +__GET__ /api/v1/default_all + +__GET__ /api/v1/defaults_settings + +__GET__ /api/v1/defaults_rules + +__GET__ /api/v1/defaults_server + +## Sample Output + +__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/map_list + + {"Maplist":["CTF-Gauntlet.unr","CTF-Command.unr","CTF-Coret.unr","CTF-Dreary.unr","CTF-LavaGiant.unr","CTF-November.unr"]} + + +__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/current_all + + {"GameName":"Capture the Flag","GameClass":"Botpack.CTFGame","LevelTitle":"Lava Giant","Level":"CTF-LavaGiant","Mutators":["Botpack.FatBoy"],"player_list":[{"PlayerName":"chacha","Ping":12,"Score":0.000000,"bIsABot":false,"bIsSpectator":true,"IP":"172.16.4.101"}]} + +__GET__ http://YOUR_SERVER_IP:YOUR_LISTEN_PORT/api/v1/default_all + + {"GameStyle":"HardCore","GameStyle":"Turbo","GameSpeed":100.000000,"AirControl":35.000000,"UseTranslocator":True,"MaxPlayers":16,"MaxSpectators":2,"bMultiWeaponStay":True,"bTournament":False,"bPlayersBalanceTeams":False,"bForceRespawn":False,"GoalTeamScore":3.000000,"TimeLimit":0,"FriendlyFireScale":0.000000,"ServerName":"Another UT Server","AdminName":"","AdminEmail":"","MOTDLine1":"","MOTDLine2":"","MOTDLine3":"","MOTDLine4":"","bWorldLog":True} \ No newline at end of file diff --git a/Run.sh b/Run.sh new file mode 100644 index 0000000..32cb01e --- /dev/null +++ b/Run.sh @@ -0,0 +1,114 @@ +#!/bin/bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +OUTPUT_DIR="$2" +DEFAULT_CFG_FILE=UnrealTournament.ini +CFG_FILE="${3:-$DEFAULT_CFG_FILE}" + +function add_iniKeyEx() { + crudini --set $OUTPUT_DIR/System/$1 $2 __$3 $4 + # Warning: ugly hack with sed to allow multiple key instances + to remove space around '=' + sed -i "s/[[:space:]]*__$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')[[:space:]]*=[[:space:]]*/$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')=/g" $OUTPUT_DIR/System/$1 +} +# !!Warning!! section is not considered +function del_iniKeyEx() { + sed -i "/[[:space:]]*$(echo $3 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')[[:space:]]*=[[:space:]]*$(echo $4 | sed -e 's/\([[\/.*]\|\]\)/\\&/g')/d" $OUTPUT_DIR/System/$1 +} +function add_iniKey() { + add_iniKeyEx $CFG_FILE $1 $2 $3 +} +# !!Warning!! section is not considered +function del_iniKey() { + del_iniKeyEx $CFG_FILE $1 $2 $3 +} +function add_ServerPackage() { + add_iniKey 'Engine.GameEngine' ServerPackages $1 + add_iniKey 'XC_Engine.XC_GameEngine' ServerPackages $1 +} +function del_ServerPackage() { + del_iniKey 'Engine.GameEngine' ServerPackages $1 + del_iniKey 'XC_Engine.XC_GameEngine' ServerPackages $1 +} +function add_ServerActors() { + add_iniKey 'Engine.GameEngine' ServerActors $1 + add_iniKey 'XC_Engine.XC_GameEngine' ServerActors $1 +} +function del_ServerActors() { + del_iniKey 'Engine.GameEngine' ServerActors $1 + del_iniKey 'XC_Engine.XC_GameEngine' ServerActors $1 +} + +function install() { + rsync -a $SCRIPT_DIR/System/ $OUTPUT_DIR/System/ --exclude '.git' + echo install ok +} + +function enable() { + add_iniKeyEx IpToCountry.ini UWeb.WebServer 'Applications[2]' ChaChaRESTStats.ChaChaRESTStats + add_iniKeyEx IpToCountry.ini UWeb.WebServer 'ApplicationPaths[2]' '/api/v1' + add_iniKeyEx IpToCountry.ini UWeb.WebServer 'bEnabled' True + + echo enable ok +} + +function disable() { + crudini --del $OUTPUT_DIR/System/$CFG_FILE UWeb.WebServer 'Applications[2]' + crudini --del $OUTPUT_DIR/System/$CFG_FILE UWeb.WebServer 'ApplicationPaths[2]' + echo disable ok +} + +function show_help() { + echo + echo "Usage: $0 { install | enable | disable } []" + echo +} + +function check_cfg_file() { + if [ -z ${CFG_FILE} ] + then + echo "CFG_FILE is unset, setting it to $DEFAULT_CFG_FILE" + CFG_FILE=$DEFAULT_CFG_FILE + else + echo "CFG_FILE is set to '$CFG_FILE'" + fi + + if [ ! -f $OUTPUT_DIR/System/$CFG_FILE ] + then + echo "$OUTPUT_DIR/System/$CFG_FILE does not exist" + show_help + exit 9999 # die with error code 9999 + fi +} + +function check_game_dir() { + ### Check if a directory does not exist ### + if [ ! -d $OUTPUT_DIR ] + then + echo "incorrect " + show_help + exit 9999 # die with error code 9999 + fi +} + +case "$1" in + 'install') + check_game_dir + install + ;; + 'enable') + check_game_dir + check_cfg_file + disable + enable + ;; + 'disable') + check_game_dir + check_cfg_file + disable + ;; + *) + show_help + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/Sources/Classes/ChaChaRESTStats.uc b/Sources/Classes/ChaChaRESTStats.uc new file mode 100644 index 0000000..615784f --- /dev/null +++ b/Sources/Classes/ChaChaRESTStats.uc @@ -0,0 +1,436 @@ +class ChaChaRESTStats expands WebApplication; + +var() class SpectatorType; +var UTServerAdminSpectator Spectator; + + +var ListItem IncludeMutators; +var ListItem ExcludeMutators; + +/* Usage: + +[UWeb.WebServer] +Applications[X]=ChaChaRESTStats.ChaChaRESTStats +ApplicationPaths[X]=/api/v1 +bEnabled=True + +http://server.ip.address/api/v1/current_game + +*/ + +event Init() +{ + Super.Init(); + + if (SpectatorType != None) + Spectator = Level.Spawn(SpectatorType); + else + Spectator = Level.Spawn(class'UTServerAdminSpectator'); + + // won't change as long as the server is up + LoadMutators(); +} + +function LoadMutators() +{ + local int NumMutatorClasses; + local string NextMutator, NextDesc; + local listitem TempItem; + local Mutator M; + local int k; + + ExcludeMutators = None; + + Level.GetNextIntDesc("Engine.Mutator", 0, NextMutator, NextDesc); + while( (NextMutator != "") && (NumMutatorClasses < 1024) ) + { + TempItem = new(None) class'ListItem'; + + k = InStr(NextDesc, ","); + if (k == -1) + TempItem.Tag = NextDesc; + else + TempItem.Tag = Left(NextDesc, k); + + TempItem.Data = NextMutator; + + if (ExcludeMutators == None) + ExcludeMutators = TempItem; + else + ExcludeMutators.AddSortedElement(ExcludeMutators, TempItem); + NumMutatorClasses++; + Level.GetNextIntDesc("Engine.Mutator", NumMutatorClasses, NextMutator, NextDesc); + } + + IncludeMutators = None; + + for ( M=Level.Game.BaseMutator.NextMutator ; M!=None ; M=M.NextMutator ) + { + TempItem = ExcludeMutators.DeleteElement(ExcludeMutators, String(M.Class)); + + if (TempItem != None) + { + if (IncludeMutators == None) + IncludeMutators = TempItem; + else + IncludeMutators.AddElement(TempItem); + } + else + log("Unknown Mutator in use: "$String(M.Class)); + } +} + +event Query(WebRequest Request, WebResponse Response) +{ + local string tmp; + //local int i; + + /* Kept in case auth needed + if(Request.Username != "test" || Request.Password != "test") + { + Response.FailAuthentication("HelloWeb"); + return; + } + */ + Response.SendStandardHeaders("application/json",False); + Response.bSentText=True; //avoid header to be re-sent + switch(Request.URI) + { + case "/map_list": + Response.SendText(EncloseJSON(QueryMapList())); + break; + case "/current_all": + tmp=QueryCurrentGame(); + tmp$=","; + tmp$=QueryCurrentPlayers(); + Response.SendText(EncloseJSON(tmp)); + break; + case "/current_game": + Response.SendText(EncloseJSON(QueryCurrentGame())); + break; + case "/current_players": + Response.SendText(EncloseJSON(QueryCurrentPlayers())); + break; + case "/default_all": + tmp=QueryDefaultsSettings(); + tmp$=","; + tmp$=QueryDefaultsRules(); + tmp$=","; + tmp$=QueryDefaultsServer(); + Response.SendText(EncloseJSON(tmp)); + break; + case "/defaults_settings": + Response.SendText(EncloseJSON(QueryDefaultsSettings())); + break; + case "/defaults_rules": + Response.SendText(EncloseJSON(QueryDefaultsRules())); + break; + case "/defaults_server": + Response.SendText(EncloseJSON(QueryDefaultsServer())); + break; + default: + Response.SendText("ERROR: Page not found or enabled."); + break; + } +} + +function string EncloseJSON(String _input) +{ + return "{"$_input$"}"; +} + +function string QueryDefaultsServer() +{ + local String tmp; + + tmp$="\"ServerName\":\""$class'Engine.GameReplicationInfo'.default.ServerName$"\","; + tmp$="\"AdminName\":\""$class'Engine.GameReplicationInfo'.default.AdminName$"\","; + tmp$="\"AdminEmail\":\""$class'Engine.GameReplicationInfo'.default.AdminEmail$"\","; + tmp$="\"MOTDLine1\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine1$"\","; + tmp$="\"MOTDLine2\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine2$"\","; + tmp$="\"MOTDLine3\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine3$"\","; + tmp$="\"MOTDLine4\":\""$class'Engine.GameReplicationInfo'.default.MOTDLine4$"\","; + tmp$="\"bWorldLog\":"$Level.Game.Default.bWorldLog; + + return tmp; +} + +function string QueryDefaultsRules() +{ + + local float FriendlyFireScale; + local class GameClass; + local String tmp; + + GameClass = Level.Game.Class; + + tmp$="\"MaxPlayers\":"$class(GameClass).Default.MaxPlayers$","; + tmp$="\"MaxSpectators\":"$class(GameClass).Default.MaxSpectators$","; + tmp$="\"bMultiWeaponStay\":"$class(GameClass).Default.bMultiWeaponStay$","; + tmp$="\"bTournament\":"$class(GameClass).Default.bTournament; + + if( class(GameClass) != None ) + { + tmp$=",\"bPlayersBalanceTeams\":"$class(GameClass).Default.bPlayersBalanceTeams; + } + + if( class(GameClass) == None ) + { + tmp$=",\"bForceRespawn\":"$class(GameClass).Default.bForceRespawn; + } + + if (class(GameClass) != None && class(GameClass) == None) + { + if (class(GameClass) != None) + { + tmp$=",\"GoalTeamScore\":"$class(GameClass).Default.GoalTeamScore; + } + else + { + tmp$=",\"FragLimit\":"$class(GameClass).Default.FragLimit; + } + + if(class(GameClass) == None) + { + tmp$=",\"TimeLimit\":"$class(GameClass).Default.TimeLimit; + } + } + + if( class(GameClass) != None && + !ClassIsChildOf( GameClass, class'CTFGame' ) && + !ClassIsChildOf( GameClass, class'Assault' ) ) + { + tmp$=",\"MaxTeams\":"$class(GameClass).Default.MaxTeams; + } + + if (class(GameClass) != None) + { + FriendlyFireScale = class(GameClass).Default.FriendlyFireScale * 100; + tmp$=",\"FriendlyFireScale\":"$FriendlyFireScale; + } + + return tmp; +} + +function string QueryDefaultsSettings() +{ + local class GameClass; + local int GameStyle; + local float GameSpeed, AirControl; + local String tmp; + + GameClass = Level.Game.Class; + + if (class(GameClass).Default.bMegaSpeed == true) + GameStyle=1; + if (class(GameClass).Default.bHardCoreMode == true) + GameStyle+=1; + + switch (GameStyle) { + case 0: + tmp$="\"GameStyle\":\"Normal\","; + break; + case 1: + tmp$="\"GameStyle\":\"HardCore\","; + case 2: + tmp$="\"GameStyle\":\"Turbo\","; + } + + GameSpeed = class(GameClass).Default.GameSpeed * 100.0; + tmp$="\"GameSpeed\":"$GameSpeed$","; + + AirControl = class(GameClass).Default.AirControl * 100.0; + tmp$="\"AirControl\":"$AirControl$","; + + tmp$="\"UseTranslocator\":"$class(GameClass).Default.bUseTranslocator; + + return tmp; +} + +function string QueryMapList() +{ + local string tmp; + local ListItem ExcludeMaps, IncludeMaps; + ReloadExcludeMaps(ExcludeMaps, String(Level.Game.Class)); + ReloadIncludeMaps(ExcludeMaps, IncludeMaps, String(Level.Game.Class)); + tmp$=RenderDataList("Maplist",IncludeMaps); + return tmp; +} + +function string QueryCurrentGame() +{ + local string tmp,LevelFullName,LevelName; + local int k; + + tmp$="\"GameName\":\""$Level.Game.GameReplicationInfo.GameName$"\","; + tmp$="\"GameClass\":\""$String(Level.Game.Class)$"\","; + + tmp$="\"LevelTitle\":\""$Level.Title$"\","; + + LevelFullName=String(Level); + k = InStr(LevelFullName, "."); + LevelName = Left(LevelFullName, k); + tmp$="\"Level\":\""$LevelName$"\","; + + if (Level.Game != None && DeathMatchPlus(Level.Game) != None && + DeathMatchPlus(Level.Game).TimeLimit > 0.0) + { + tmp$="\"TimeLimit\":"$DeathMatchPlus(Level.Game).TimeLimit$","; + } + tmp$=RenderDataList("Mutators",IncludeMutators); + + return tmp; +} + +function string QueryCurrentPlayers() +{ + local string tmp; + local Pawn P; + local string IP; + local bool bFirst; + + tmp$="\"player_list\":["; + bFirst=True; + for (P=Level.PawnList; P!=None; P=P.NextPawn) { + if (P.bIsPlayer + && !P.bDeleteMe + && UTServerAdminSpectator(P) == None + && P.PlayerReplicationInfo != None) + { + if(!bFirst) + tmp$=","; + bFirst=False; + tmp$="{"; + tmp$="\"PlayerName\":\""$P.PlayerReplicationInfo.PlayerName$"\","; + tmp$="\"Ping\":"$max(P.PlayerReplicationInfo.Ping, 0)$","; + tmp$="\"Score\":"$P.PlayerReplicationInfo.Score$","; + if (P.PlayerReplicationInfo.bIsABot) + { + tmp$="\"bIsABot\":true,"; + tmp$="\"bIsSpectator\":false,"; + } + else + { + tmp$="\"bIsABot\":false,"; + if (P.PlayerReplicationInfo.bIsSpectator) + { + tmp$="\"bIsSpectator\":true,"; + } + else + { + tmp$="\"bIsSpectator\":false,"; + } + } + IP = ""; + if ( PlayerPawn(P) != None ) + { + IP = PlayerPawn(P).GetPlayerNetworkAddress(); + IP = class'InternetInfo'.static.StripPort(IP); + } + tmp$="\"IP\":\""$IP$"\""; + tmp$="}"; + } + } + tmp$="]"; + return tmp; +} + +function string RenderDataList(String name,ListItem _list) +{ + local string tmp; + local ListItem TempItem; + local bool bFirst; + tmp$="\""$name$"\":["; + bFirst = true; + for (TempItem = _list; TempItem != None; TempItem = TempItem.Next) { + if(!bFirst) + tmp$=","; + tmp$="\""$TempItem.Data$"\""; + bFirst=false; + } + tmp$="]"; + return tmp; +} + +function ReloadExcludeMaps(out ListItem ExcludeMaps, String GameType) +{ + local class GameClass; + local string FirstMap, NextMap, TestMap; + local ListItem TempItem; + + GameClass = class(DynamicLoadObject(GameType, class'Class')); + + ExcludeMaps = None; + if(GameClass.Default.MapPrefix == "") + return; + FirstMap = Level.GetMapName(GameClass.Default.MapPrefix, "", 0); + NextMap = FirstMap; + while (!(FirstMap ~= TestMap) && FirstMap != "") + { + if(!(Left(NextMap, Len(NextMap) - 4) ~= (GameClass.Default.MapPrefix$"-tutorial"))) + { + // Add the map. + TempItem = new(None) class'ListItem'; + TempItem.Data = NextMap; + + if(Right(NextMap, 4) ~= ".unr") + TempItem.Tag = Left(NextMap, Len(NextMap) - 4); + else + TempItem.Tag = NextMap; + + if (ExcludeMaps == None) + ExcludeMaps = TempItem; + else + { + // Maplists returned by GetMapName get sorted in C++ as of the Unreal Tournament 469 patch + //ExcludeMaps.AddSortedElement(ExcludeMaps, TempItem); + ExcludeMaps.AddElement(TempItem); + } + } + + NextMap = Level.GetMapName(GameClass.Default.MapPrefix, NextMap, 1); + TestMap = NextMap; + } +} + +function ReloadIncludeMaps(out ListItem ExcludeMaps, out ListItem IncludeMaps, String GameType) +{ + local class GameClass; + local ListItem TempItem; + local int i; + + GameClass = class(DynamicLoadObject(GameType, class'Class')); + if(GameClass.Default.MapListType == None) + return; + if (GameClass != None) + { + for (i=0; i+WLg4187`W9+B*eD~E` z#$G=D*n1&mNL32=)t0r1Ulh$QB%UMkRrQ@XWls{yT4{yOKcx5*j{m?E=+28r?TdDjGe>WZ*_mWw%E~9wso~U2ngG?q zsl1&{*?H9x<}NOW97HY(F+}WDwL-X!*!o7Tg%p*D=9uiu+Cz5!7{nY%9kg>(gi)dQ zU}$nU#}y!0<$*+YAf3u*)7*|q9+`@tPjzVJh;>O1ju7e!uCb?yQE2j1I-8t6nx3U< z4PYz3aW`)O)i{G=c77q9y#QUDwR55mMj^n7a?S84eR{;l2-Qg1K++QE5bX1b)X=<@ z;A)3Pj!z68O{DA|jxDP@-PPSxtrS{FpZUk9M7XdD@Xmq$G#@S1P*FAzr4L=pOKS84 zQ_$x02A0%tsS=)7ff<5^(X2g_xI|p4`mLO;cWV_2&>Jd1nT$1c0nxpoNk05Wc$5n} zlx*dMUBo;zp3f#SqcDi5>LDJC2bJnyPl77~BVf$VWzwmf;7tv0DcB-&RE|#pv%E}a ztZK~87qY37R$q{?ja)P&l+-_w^ zaQ(q-0y0JN^1(#b9-p#=$eKfGD~U3lXsI8(oU-Nq%h-(gnxjghV)hZ^y zhq6+CtS~RSmx5X3?%;$ypBWZeka;KOIcYVDL&Hat1#W5Wpl#*nMy&kQoNB8Wm^i4S zvEo23hcgU`UVGF+(J4xLa(vZIp2*n~Su2%GrXg2}R8}=Pk%s;RSz`LAn;Zc)iBq-( zK>-Xd6UUm#gX!!PB)VKi=AXt%krn!_q?MYowQe~v!LtbsR-*Uuz=P@1Kat3D&w2wx zsaY7hh~V{3Sd<35elgeqwPF+ofHeyUIL$&-k7m;uJDVq!yfuXAYO3X(;cBZn2(wh) z)l`Ym++UcP;WM#!y-Q#B$PeO}Ry|ZD=OH=URmq7%HUyk_@P^2KHRP)M)9Iv35n$)m zEO0)N$`oWY6;e|;rI7hGG$$M^B$J9?V3OjE{2RnTR*Bz#J}GB@#=N*}p`Szth0GmJ%fg#@%KfJ;57)M!}Sv5ddoM}YnoEM3L_Tr`YzVH;l+ zv$cdoD`XbnWdJ?T*ai)7kaWDtv2Isbz;>60)yv)##2ET`Gh;8nXN}d*sxq0D2<9;i z>_~5fH8nLEkNF^Oe;)t)j^P7^KNoI^#8^1&BA^6-6enF2A1KD9p+jkh*dudhd{JQ4 zvX+y^W3{a7A=c6oXW?2Fzsg!zmm`YEk&?v)P2f@E+O-9#Wk-0!dbSXM)TxIzY-69k zqseJPLq2>9dQ>w(O-+$rHeQSzZc*koSG0(!Bgc%!aM;VLyp_PpgZx;(!FE@i0$(<4 zW$eSyfG%2(K4ok-=-Yb+WUlLCTiR;bI<_nD9=5>Nb%X=Q*~8FCR<1``4p|dhhYorQ zhy`gBNU7Cpe9!QAaS<)&7^t#dQ>BV}+P5Fo+QT;2cED%T8qf$dtu-n;z(HGJ+oSSUo~N5DvU- zEU*gj`lpR7Z~-kEL5r^%O-3X1WOD#?@c*mv!%WMG)P+=fA!Q2Jn2D5G$l1Lngt+7d zGU`R;lm63&=KuAY|M#*xjmn=v2VC9ASOz|ut68*ooHW<4C~Gfn#YkwwptE3A2g^X# zjxpEf2zsHDvAXrZ^GOhRCwy*tAspyr3q~hewjDfK!Ms-3=nVV&!E36)d?wsn8|Hi} zfQ%h5*dfFB7f{*{>^=nCYM~3|I3m{t_OXAdZ+Mi|`kA-O^9M#7m~UYDT6an$WEpS z;kDOlS?ecAH_+7cRRhNN%l$Nts%1)}l_m`D6RedF=vubbA^AoziL)monypR}sD|-e zw0r}U3CA|NyA4Gr(u^ee2#d<1;h+!45)Jy6d!z~J$G z2nLLlim6GyisI?qGiZEOY$wr8l!3;qwr3n;`@*KxS%U zfhHIp>su^m7t?)z4%S`KFl4;|2A=U{qs<4HJ5c$TLTT30J#S_BDhp8Rf!@<>G*^PH z{)8v=6@X9i!;3HgTeZc7VoX(7&a)oeci>lC8BN3+=ws3U=Q>f3k~mOMhA7NH`5Y}l-rPT zLck*l-$ueKMz-POZbE%?=N9#cNHOZUwt&{dN~eZp8$N}laNuuB6mad7fzOG0)DV64 zEnwrXr-VwXs$Du@IiCGJF05ER*Fs5k!-qkI&w5}T#=Wp%EqmDVFvI*4U=mifPeFxu zC^$$S9&n*TwLe0km1_i>TDA=mv;h6FaYt<0Aul-}Koi+EOuSwv!LVD|9U)xpCt*VP z8YCDL?`etfp-MS}7Grjpjv2i zWH>)SfBk2ZgXcAV-vF~w1Q&)}>39S^;zImg5z~PeVU!6~$*>oNTg1Uzq&J@}*ucHhgr&|MP#=Nx8)cE`Bvr$DMel#&B~Mg) zG^}4Q<|UDKYDk}13Tc;y^y#ILc56t#>Oz9PmnY5;Z4)YYQ-b>)7$9C2(nez+i`Bdj zT-XN_$#d{o%hu8|s2i3+fgYBvXKOJ*{Za?4fC4vQnZ6c#yKWnluRY1?HpB4>>_{SP zaU-j%m&oY#U&a+sE%P<8SQOX$ELMUM4$Q%V#fJ=iY60ZKarRURZs0Jihng6Nx>iNh z^z>B;d*X^L(UQw=;|hu6NPz$SM&MzPd<#mxh54Y~#BSkBB_Hf|>caj3ZAk?BK&*+$ zvQ{haV>^xO0QzPXoc6#c&UTB8?F_d-SZh885dPfvqA`Uh^=8q{mT6zg!0k)UbRQM( zsSZ_#E?U`WyGdkw%OAt-Gu80KQ(>S28nZmEgG$Ufsyx9n<>8&U6J@RCf`K!htRkbz7N4_r;#-(Bu$b z%=Aq_Dnx;E`Fy6gyPL0&;)zV$;x{LPLai}zrHJ?)-S|J;O{MZ+Y$6|RuMRC%hl1k; zI5v&*7II85F=IwuZZ)^`nRulM6v3c^n0+Rle#FP|*w;{brk`srR;?$1p^*gd zlVTi)BYw17oKyp^po*Vjq@(ssUSobigo=ZP*F}PvaEQEpVr&NXN>Ov9Pe_M^5!db@<9gWa zF-NxaA*BhWz7ZF!T(lHB%Jo#B)Ju3fnVOA`#0PM$XqP>U{U$rYm5dfKPAIBM?9;oo zjP;Tox!vj5VodB=@vuQ@z`GFKs)cAWJsXWIbwC$sEn`;@7*N;nluS<&qA-7m>rqpD z#&BYkx1`X>uVca0Xu2yq<>xbKOn0}F(h}H!Z5`$jJCnx~!zs92HmyQ_4hFJ4ZBGL< zF%uQS;&6;a_kENe@^D8PF&}(TsuCZCK7l*>X$PvhTq+hn2)FMCfIUnz;?+Yma@bBL z(_n^30$I8xM%AtaQ9GQO)J1T-%!7<*r1-yxvvyYwbOVXGg#_G~I;M{eD{II}5p|3cff*jcObJ32@tQEIz3z!p2g0!^ z=EG<^g)Y!cW^D_zd@v|@YDYhUE%aL-5sSw*c-=vh7mx$2wGXlLv4i?O!2@Db!u6oaI#k+UL&NVe(>VAWg| z0Zl*hzX1eQjGu^c`)7`I|>F!dd>W~DOMq0IH zpNP@8Lg3*jf}Mxw%_Cf72pT95%sj+r@TrYia!d*W1OngZV@%`g#(gH1Xc_3BxP}Q1 zWQ7CkSX&l&8K@UUr>0ub zYd4p6un(oJ45W0v&2_ZXFFiE1RuC|*$8fqvX7px3r`!d*a*it=?v>NJ-IQA6?r3HG zKs4M`xwJeRab3)4w%)W0T!d&TI-l=}P`6?^>RLyWu>yZ8GARLBL+EjfSJ1uKyT!Vw zTcg$KqR6B!CE>P|)3mMybxio#GJsp$;oJZm&P*Sli8}c%CQeqw6V5ghk7>||o0Eum zK@ulER8-3=F3sqr^i|_b(v*wjrHG}mjtV7dAM*9`A*&Q9CGCLIsU6e<%CZt9GO?u9 zF=CZ7nzO9dH{RTWWoIlr+* z+!+t#@al_3iqf{P2US@eOZiF$pIn#c+DUAIG2echH0P3c+Ra61rfu>LTH*8Fg#;!2 z2c3FWJl2sbT4#ezggwg;O5G$W5$s%sfS&V6Y)ozNPj#6J;1zhg~LxBr|6?D#}DH{ zJ7=iKLugZ;H^P(an7Y$}3Zj;LCrO<@5;h-h(c)z!--U8odB_O(ZdwfxGnZvVPIZ3^ zD++H9=rvs=rtzV(trhyobr4hDS240nz7gj&+#EVS)Gj1s&to)dn@`O(2r1fn#wB*? zegh#i-O&hE$DtZOg0?%9&|Ojq!lDR7LQygG4-Uu7~!ow^wR!&=iMe0jp; z-*`YgX#t#j@Iyy>SOcCoLkFoGkslHRD&2mJipviahRI})Im+)jpwKr;*Bl7qa};&$ zXdIrIa3g578w^6vZh=<$bu6c~{1s<(v>m5N+JS%?@B<7+6vA<%%h=fqNT@wR!7ObV(L~1b3A%&Rs{^ zpane;BKoCh3Bw`8(X|w+c>lwlC6@kb+zKGbqSrpc8#-8R%>jqMAQDKly`j4z-nWaq z+hn40IJ>Asw3O{vj3FgXbfe(9ltwPCxklRc)lG=qig9ZG|L`bey@)UroojN1G3qf= z#S;M9$;<6jkceD(@g=^`DG$dv39TO5hP5xjw#9SsZQaCF9Md*^KQTHyN{L16DwR8i zN1(P&pjEtM&JoJZlT*>8%;yg?1@l2nEiWjMKtG|I`AS}7BM`Mm zwJCJ{l98hqWhxmukyez0_PBBsbP0Aha;7XL`9V8rvPWGj%l^??`V^4Tmr7mktid%9 z!_iSkN{ZuMy5nb&R49y4t5tZpboG@kowS8PJ=Mk?clK?xa0FhOWx!3` zx1`}Q7TN&UW znIU6(A7VzgL}jD-5C#vqik^t5cK&JvNET5woPl8VB3@&FDlt$j#h;m2nRc_wgf}F> zYB}v%BkJ1#5x6x)w!|A>{=nTkz4Dp2GI0c`7apMha?emSOp0H0J6zX-f?zE%0?o$~ zvvd3z2`SDsjo{Y}xZfRV;%&-J3`Iw34+4~~+RGM~0?tbm*iFcV0NEcb3K=albqU-} z=qKQtgf&YJF1PXf*Y>nIdD(k=B%AJbM$u;@CCp?XD2?NO3#pQPo&_6I%ztG zyXNlOZZ|XdWnwZ2_-g^Y^g|cgLPUyqpSGROdazV6xn%wWvG0)Aim2=34;lihcaw$lFa7&ipG5dyPq$M*C>Vf*Z zVv$D_DvN6ZM+xmkAjKg~Pl>)Ad(6b%1KF%~c>unIk#`966ek7gZ8^#_ioXsji+(0? z4yQbr{>?;Z=gQND9?Rw5B2gf(N)Zw5=pZ=&Tt1X5PzQLk?Byg6@sw><`tX2(RH3q* zluX$$CbF_C*EtgTLp(}$MP!)UT}?d0V{qtssU&f_XgD^m8?@9?Rhksl(<}n1 z#Z9SrZE&t2>!_>%kCfZg`{sACVPnYf;@3#=hn_Ht|9FdR#i!53Ka-v#26Fm`#Gn5+ ze!GsDO<%n9h{S?#ku&y_ijVzpnU)VC&ilGv{^?3^-iF8nk9_>w|98kr#HtFV`n(fW z{lZI6f2$N!+OiT^%P?GqTv6fQHelml500&*@FXHHZawjmLjGU2JckTFzvE1evi&32 z@{#DP?hpgU8(f>e`qQ7Dt?=YJ^r1~epeNq$CmcL?tWRdyio-u zeVO5@KnhtER2;(J^?>h)hkx9v3W{o(76noaRbh>HL*x6`uJb*!(Z?F#`@BBJ8W%8m zv%Db+jpB1iHoV1XylDJ4Oa%{N;b%~|C_x*b;iu3rhY*}BO!2uzU|vGNe*oYviLtry z0pnF@*d#Ew3d{!(0eQ_zk`XMRxPu;``8O!+6AcGEP=NYtEsd9q|ANnxl6$1_vhnxO z02*?P?E<4<0FQqr5^l^D$$b!+p_t1C;Ql)l`X%up1K4590i$uy^AjkXMV!BdLI)6` zUB@JDvk-WT;I=6F43%@-Yx2_;`27~+3x{_?*qax3PUIEm!7x-P_Q9~#-WvQp+y(~ zuRwhQg`&^e;PW5h^8_{^W>@2)f!g#!9W6R&Km!6|!cS+ACt~aX40PN)l49YUEZidt zDI`WvRG=g!B#n@N0?2*H4hyFdgoTU*oku^TdlS-(PC!A?d%+6UEpIq4ig_d#`>L-SC5vx9tK+jW8pR>z<^2vAYIXWQfS7Z;mI>Jb$~gj z?sckf1zZ#>v^cW;20^(wcS8+T~FhH3J!b7@{zX3;ZG?j zwx^+b1gZ^OBfv#3^$=VL+Vcj4pI=%Quqc3mw;4uVd?A=i^ir`j3jL(C}O*w|$Cpy1(-V|67~L zspcZ!V%G@L!Pv9Z$-?xzV%)eRob()25&0CaCSp9c#;96d04RolfcAG?XNRFW#GQJq zi}&Q6ym|(p*jri7YOd=EQYwf_jr;PVT7lIgo{gCki_pC9-di~<3Ejh? zl?Xj&+`H$A7cgY%uzJ{7+{>Zx$4UU|8`#II)mXjTd*8qlUqWQ~7Hfdb1TG!o0VX)# zdz=qEQN8~-r}ZQ9RPdsxhOkH9tSI1%Q+|80N8b{`NGzj#C8y@y9Fao|758t;P+A#1T&;I~npL5{= literal 0 HcmV?d00001