From e19ac600ffbb2ff2c7d7840148295b79479bea05 Mon Sep 17 00:00:00 2001 From: Regalis Date: Tue, 18 Aug 2015 22:29:21 +0300 Subject: [PATCH] v0.1.3.2 --- Subsurface/Content/Jobs.xml | 4 +- Subsurface/Properties/AssemblyInfo.cs | 4 +- .../Source/Characters/AI/EnemyAIController.cs | 6 +- Subsurface/Source/Characters/Character.cs | 24 ++++---- Subsurface/Source/GUI/GUITextBlock.cs | 2 + Subsurface/Source/GUI/GUITickBox.cs | 23 ++++++-- Subsurface/Source/Networking/GameClient.cs | 24 +++++--- Subsurface/Source/Networking/GameServer.cs | 28 +++++---- Subsurface/Source/Screens/MainMenu.cs | 55 ++++++++++++++---- Subsurface/Source/Screens/NetLobbyScreen.cs | 10 +++- Subsurface/changelog.txt | 14 +++++ Subsurface/readme.txt | 25 ++++++++ Subsurface_Solution.v12.suo | Bin 654336 -> 648192 bytes 13 files changed, 161 insertions(+), 58 deletions(-) diff --git a/Subsurface/Content/Jobs.xml b/Subsurface/Content/Jobs.xml index a9fc829e4..8316e2e55 100644 --- a/Subsurface/Content/Jobs.xml +++ b/Subsurface/Content/Jobs.xml @@ -13,7 +13,7 @@ - + @@ -24,7 +24,7 @@ - + diff --git a/Subsurface/Properties/AssemblyInfo.cs b/Subsurface/Properties/AssemblyInfo.cs index 511733048..2feb084e1 100644 --- a/Subsurface/Properties/AssemblyInfo.cs +++ b/Subsurface/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.3.1")] -[assembly: AssemblyFileVersion("0.1.3.1")] +[assembly: AssemblyVersion("0.1.3.2")] +[assembly: AssemblyFileVersion("0.1.3.2")] diff --git a/Subsurface/Source/Characters/AI/EnemyAIController.cs b/Subsurface/Source/Characters/AI/EnemyAIController.cs index ffdba403f..d0604970f 100644 --- a/Subsurface/Source/Characters/AI/EnemyAIController.cs +++ b/Subsurface/Source/Characters/AI/EnemyAIController.cs @@ -482,9 +482,9 @@ namespace Subsurface } message.Write(MathUtils.AngleToByte(steeringManager.WanderAngle)); - message.WriteRangedSingle(Math.Max(updateTargetsTimer,0.0f), 0.0f, UpdateTargetsInterval, 8); - message.WriteRangedSingle(Math.Max(raycastTimer,0.0f), 0.0f, RaycastInterval, 8); - message.WriteRangedSingle(Math.Max(coolDownTimer, 0.0f), 0.0f, attackCoolDown*2.0f, 8); + message.WriteRangedSingle(MathHelper.Clamp(updateTargetsTimer,0.0f, UpdateTargetsInterval), 0.0f, UpdateTargetsInterval, 8); + message.WriteRangedSingle(MathHelper.Clamp(raycastTimer, 0.0f, RaycastInterval), 0.0f, RaycastInterval, 8); + message.WriteRangedSingle(MathHelper.Clamp(coolDownTimer, 0.0f, attackCoolDown * 2.0f), 0.0f, attackCoolDown * 2.0f, 8); message.Write(targetEntity==null ? -1 : (targetEntity as Entity).ID); } diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index 85b126a30..37462d66e 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -634,7 +634,6 @@ namespace Subsurface } } - if (AnimController.onGround && !AnimController.InWater && AnimController.Anim != AnimController.Animation.UsingConstruction) @@ -1037,15 +1036,15 @@ namespace Subsurface message.Write(NetTime.Now); // Write byte = move direction - message.WriteRangedSingle(AnimController.TargetMovement.X, -10.0f, 10.0f, 8); - message.WriteRangedSingle(AnimController.TargetMovement.Y, -10.0f, 10.0f, 8); + message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.X, -10.0f, 10.0f), -10.0f, 10.0f, 8); + message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.Y, -10.0f, 10.0f), -10.0f, 10.0f, 8); message.Write(AnimController.TargetDir==Direction.Right); - if (aiController!=null) + if (aiController==null) { - message.WriteRangedSingle(cursorPosition.X, -1000.0f, 1000.0f, 16); - message.WriteRangedSingle(cursorPosition.Y, -1000.0f, 1000.0f, 16); + message.Write(cursorPosition.X); + message.Write(cursorPosition.Y); } message.Write(LargeUpdateTimer <= 0); @@ -1061,12 +1060,12 @@ namespace Subsurface message.Write(limb.body.LinearVelocity.X); message.Write(limb.body.LinearVelocity.Y); - message.Write(MathUtils.AngleToByte(limb.body.Rotation)); + message.Write(limb.body.Rotation); message.Write(limb.body.AngularVelocity); i++; } - message.WriteRangedSingle(AnimController.StunTimer, 0.0f, 60.0f, 8); + message.WriteRangedSingle(MathHelper.Clamp(AnimController.StunTimer,0.0f,60.0f), 0.0f, 60.0f, 8); message.Write((byte)health); if (aiController != null) aiController.FillNetworkData(message); @@ -1140,13 +1139,12 @@ namespace Subsurface targetDir = message.ReadBoolean(); - if (aiController!=null) + if (aiController==null) { cursorPos = new Vector2( - message.ReadRangedSingle(-1000.0f, 1000.0f, 16), - message.ReadRangedSingle(-1000.0f, 1000.0f, 16)); + message.ReadFloat(), + message.ReadFloat()); } - } catch @@ -1181,7 +1179,7 @@ namespace Subsurface vel.X = message.ReadFloat(); vel.Y = message.ReadFloat(); - rotation = MathUtils.ByteToAngle(message.ReadByte()); + rotation = message.ReadFloat(); angularVel = message.ReadFloat(); } catch diff --git a/Subsurface/Source/GUI/GUITextBlock.cs b/Subsurface/Source/GUI/GUITextBlock.cs index 3cc5f0da1..3edce2fd4 100644 --- a/Subsurface/Source/GUI/GUITextBlock.cs +++ b/Subsurface/Source/GUI/GUITextBlock.cs @@ -80,6 +80,8 @@ namespace Subsurface : this (rect, text, null, null, alignment, textAlignment, style, parent, wrap) { this.Font = font == null ? GUI.Font : font; + + SetTextPos(); } public GUITextBlock(Rectangle rect, string text, Color? color, Color? textColor, Alignment textAlignment = Alignment.Left, GUIStyle style = null, GUIComponent parent = null, bool wrap = false) diff --git a/Subsurface/Source/GUI/GUITickBox.cs b/Subsurface/Source/GUI/GUITickBox.cs index 1294417ea..a36d02855 100644 --- a/Subsurface/Source/GUI/GUITickBox.cs +++ b/Subsurface/Source/GUI/GUITickBox.cs @@ -41,19 +41,26 @@ namespace Subsurface box.HoverColor = Color.Gray; box.SelectedColor = Color.DarkGray; - text = new GUITextBlock(new Rectangle(rect.X + 40, rect.Y, 200, 30), label, Color.Transparent, Color.White, Alignment.TopLeft, null, this); + text = new GUITextBlock(new Rectangle(rect.X + 40, rect.Y, 200, rect.Height), label, Color.Transparent, Color.White, Alignment.TopLeft, null, this); Enabled = true; } public override void Update(float deltaTime) { - base.Update(deltaTime); + if (rect.Width ==420) + { + int asd = 1; + } + //base.Update(deltaTime); if (!Enabled) return; if (box.Rect.Contains(PlayerInput.GetMouseState.Position)) { + + + box.State = ComponentState.Hover; if (PlayerInput.GetMouseState.LeftButton == ButtonState.Pressed) @@ -77,12 +84,16 @@ namespace Subsurface public override void Draw(SpriteBatch spriteBatch) { + if (rect.Width == 420) + { + int asd = 1; + } + DrawChildren(spriteBatch); - if (Selected) - { - GUI.DrawRectangle(spriteBatch, new Rectangle(box.Rect.X + 2, box.Rect.Y + 2, box.Rect.Width - 4, box.Rect.Height - 4), Color.Green * 0.8f, true); - } + GUI.DrawRectangle(spriteBatch, new Rectangle(box.Rect.X + 2, box.Rect.Y + 2, box.Rect.Width - 4, box.Rect.Height - 4), + selected ? Color.Green * 0.8f : Color.Black, true); + } } } diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index 47fcbd36a..7e9b882ce 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -74,8 +74,8 @@ namespace Subsurface.Networking // Create new instance of configs. Parameter is "application Id". It has to be same on client and server. NetPeerConfiguration Config = new NetPeerConfiguration("subsurface"); - Config.SimulatedLoss = 0.2f; - Config.SimulatedMinimumLatency = 0.5f; + //Config.SimulatedLoss = 0.2f; + //Config.SimulatedMinimumLatency = 0.5f; // Create new client, with previously created configs client = new NetClient(Config); @@ -111,8 +111,11 @@ namespace Subsurface.Networking //update.Elapsed += new System.Timers.ElapsedEventHandler(Update); // Funtion that waits for connection approval info from server + if (reconnectBox==null) + { + reconnectBox = new GUIMessageBox("CONNECTING", "Connecting to " + serverIP, new string[0]); + } - reconnectBox = new GUIMessageBox("CONNECTING", "Connecting to " + serverIP, new string[0]); CoroutineManager.StartCoroutine(WaitForStartingInfo()); // Start the timer @@ -239,14 +242,19 @@ namespace Subsurface.Networking if (!connected || updateTimer > DateTime.Now) return; - if (client.ConnectionStatus == NetConnectionStatus.Disconnected && reconnectBox==null) + if (client.ConnectionStatus == NetConnectionStatus.Disconnected) { - reconnectBox = new GUIMessageBox("CONNECTION LOST", "You have been disconnected from the server. Reconnecting...", new string[0]); - connected = false; - ConnectToServer(serverIP); + if (reconnectBox==null) + { + reconnectBox = new GUIMessageBox("CONNECTION LOST", "You have been disconnected from the server. Reconnecting...", new string[0]); + connected = false; + ConnectToServer(serverIP); + } + return; } - else if (reconnectBox!=null) + + if (reconnectBox!=null) { reconnectBox.Close(null,null); reconnectBox = null; diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 95ba6e8ad..4472cbf9a 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -30,7 +30,7 @@ namespace Subsurface.Networking private Client myClient; - public GameServer(string name, int port, bool isPublic = false, string password="") + public GameServer(string name, int port, bool isPublic = false, string password="", bool attemptUPnP = false, int maxPlayers = 10) { var endRoundButton = new GUIButton(new Rectangle(Game1.GraphicsWidth - 290, 20, 150, 25), "End round", Alignment.TopLeft, GUI.style, inGameHUD); endRoundButton.OnClicked = EndButtonHit; @@ -41,26 +41,30 @@ namespace Subsurface.Networking config = new NetPeerConfiguration("subsurface"); - config.SimulatedLoss = 0.2f; - config.SimulatedMinimumLatency = 0.5f; + //config.SimulatedLoss = 0.2f; + //config.SimulatedMinimumLatency = 0.5f; config.Port = port; Port = port; - config.EnableUPnP = true; + if (attemptUPnP) + { + config.EnableUPnP = true; + } - config.MaximumConnections = 10; - + config.MaximumConnections = maxPlayers; + config.EnableMessageType(NetIncomingMessageType.ConnectionApproval); try { server = new NetServer(config); server.Start(); - - // attempt to forward port - server.UPnP.ForwardPort(port, "subsurface"); + if (attemptUPnP) + { + server.UPnP.ForwardPort(port, "subsurface"); + } } catch (Exception e) @@ -126,7 +130,7 @@ namespace Subsurface.Networking masterServerResponded = false; var restRequestHandle = client.ExecuteAsync(request, response => MasterServerCallBack(response)); - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 15); while (!masterServerResponded) { if (DateTime.Now > timeOut) @@ -220,9 +224,9 @@ namespace Subsurface.Networking break; } - if (!isClient) + if (!isClient && (c.SimPosition==Vector2.Zero || c.SimPosition.Length() < 300.0f)) { - //c.LargeUpdateTimer = 0; + c.LargeUpdateTimer -= 2; new NetworkEvent(c.ID, false); } } diff --git a/Subsurface/Source/Screens/MainMenu.cs b/Subsurface/Source/Screens/MainMenu.cs index 781fcfea7..237d7b75a 100644 --- a/Subsurface/Source/Screens/MainMenu.cs +++ b/Subsurface/Source/Screens/MainMenu.cs @@ -18,8 +18,8 @@ namespace Subsurface private GUITextBox saveNameBox, seedBox; - private GUITextBox serverNameBox, portBox, passwordBox; - private GUITickBox isPublicBox; + private GUITextBox serverNameBox, portBox, passwordBox, maxPlayersBox; + private GUITickBox isPublicBox, useUpnpBox; private Game1 game; @@ -113,18 +113,41 @@ namespace Subsurface new GUITextBlock(new Rectangle(0, -25, 0, 30), "Host Server", GUI.style, Alignment.CenterX, Alignment.CenterX, menuTabs[(int)Tabs.HostServer], false, GUI.LargeFont); - new GUITextBlock(new Rectangle(0, 30, 0, 30), "Server Name:", GUI.style, Alignment.CenterX, Alignment.CenterX, menuTabs[(int)Tabs.HostServer]); - serverNameBox = new GUITextBox(new Rectangle(0, 60, 200, 30), null, null, Alignment.CenterX, Alignment.CenterX, GUI.style, menuTabs[(int)Tabs.HostServer]); + new GUITextBlock(new Rectangle(0, 50, 0, 30), "Server Name:", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); + serverNameBox = new GUITextBox(new Rectangle(160, 50, 200, 30), null, null, Alignment.TopLeft, Alignment.Left, GUI.style, menuTabs[(int)Tabs.HostServer]); - new GUITextBlock(new Rectangle(0, 100, 0, 30), "Server port:", GUI.style, Alignment.CenterX, Alignment.CenterX, menuTabs[(int)Tabs.HostServer]); - portBox = new GUITextBox(new Rectangle(0, 130, 200, 30), null, null, Alignment.CenterX, Alignment.CenterX, GUI.style, menuTabs[(int)Tabs.HostServer]); + new GUITextBlock(new Rectangle(0, 100, 0, 30), "Server port:", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); + portBox = new GUITextBox(new Rectangle(160, 100, 200, 30), null, null, Alignment.TopLeft, Alignment.Left, GUI.style, menuTabs[(int)Tabs.HostServer]); portBox.Text = NetworkMember.DefaultPort.ToString(); portBox.ToolTip = "Server port"; - isPublicBox = new GUITickBox(new Rectangle(portBox.Rect.X - menuTabs[(int)Tabs.HostServer].Rect.X, 200, 20, 20), "Public server", Alignment.TopLeft, menuTabs[(int)Tabs.HostServer]); + new GUITextBlock(new Rectangle(0, 150, 100, 30), "Max players:", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); + maxPlayersBox = new GUITextBox(new Rectangle(195, 150, 30, 30), null, null, Alignment.TopLeft, Alignment.Center, GUI.style, menuTabs[(int)Tabs.HostServer]); + maxPlayersBox.Text = "8"; + maxPlayersBox.Enabled = false; + + var plusPlayersBox = new GUIButton(new Rectangle(230, 150, 30, 30), "+", GUI.style, menuTabs[(int)Tabs.HostServer]); + plusPlayersBox.UserData = 1; + plusPlayersBox.OnClicked = ChangeMaxPlayers; + + var minusPlayersBox = new GUIButton(new Rectangle(160, 150, 30, 30), "-", GUI.style, menuTabs[(int)Tabs.HostServer]); + minusPlayersBox.UserData = -1; + minusPlayersBox.OnClicked = ChangeMaxPlayers; + + new GUITextBlock(new Rectangle(0, 200, 0, 30), "Password (optional):", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); + passwordBox = new GUITextBox(new Rectangle(160, 200, 200, 30), null, null, Alignment.TopLeft, Alignment.Left, GUI.style, menuTabs[(int)Tabs.HostServer]); + + + isPublicBox = new GUITickBox(new Rectangle(10, 250, 20, 20), "Public server", Alignment.TopLeft, menuTabs[(int)Tabs.HostServer]); + + useUpnpBox = new GUITickBox(new Rectangle(10, 300, 20, 20), "Attempt UPnP port forwarding", Alignment.TopLeft, menuTabs[(int)Tabs.HostServer]); + new GUITextBlock(new Rectangle(0, 330, 0, 30), + "UPnP can be used for forwarding ports on your router to allow players join the server." + + " However, UPnP isn't supported by all routers, so you may need to setup port forwards manually" + +" if players are unable to join the server (see the readme for instructions).", + GUI.style, Alignment.TopLeft, Alignment.TopLeft, menuTabs[(int)Tabs.HostServer], true, GUI.SmallFont); + - new GUITextBlock(new Rectangle(0, 240, 0, 30), "Password (optional):", GUI.style, Alignment.CenterX, Alignment.CenterX, menuTabs[(int)Tabs.HostServer]); - passwordBox = new GUITextBox(new Rectangle(0, 270, 200, 30), null, null, Alignment.CenterX, Alignment.CenterX, GUI.style, menuTabs[(int)Tabs.HostServer]); GUIButton hostButton = new GUIButton(new Rectangle(0, 0, 200, 30), "Start", Alignment.BottomCenter, GUI.style, menuTabs[(int)Tabs.HostServer]); hostButton.OnClicked = HostServerClicked; @@ -163,6 +186,18 @@ namespace Subsurface return true; } + private bool ChangeMaxPlayers(GUIButton button, object obj) + { + int currMaxPlayers = 10; + + int.TryParse(maxPlayersBox.Text, out currMaxPlayers); + currMaxPlayers = MathHelper.Clamp(currMaxPlayers+(int)button.UserData, 1, 10); + + maxPlayersBox.Text = currMaxPlayers.ToString(); + + return true; + } + private bool HostServerClicked(GUIButton button, object obj) { string name = serverNameBox.Text; @@ -181,7 +216,7 @@ namespace Subsurface return false; } - Game1.NetworkMember = new GameServer(name, port, isPublicBox.Selected, passwordBox.Text); + Game1.NetworkMember = new GameServer(name, port, isPublicBox.Selected, passwordBox.Text, useUpnpBox.Selected, int.Parse(maxPlayersBox.Text)); Game1.NetLobbyScreen.IsServer = true; Game1.NetLobbyScreen.Select(); diff --git a/Subsurface/Source/Screens/NetLobbyScreen.cs b/Subsurface/Source/Screens/NetLobbyScreen.cs index 9cbcae64f..dbcf83bde 100644 --- a/Subsurface/Source/Screens/NetLobbyScreen.cs +++ b/Subsurface/Source/Screens/NetLobbyScreen.cs @@ -309,9 +309,11 @@ namespace Subsurface jobList = new GUIListBox(new Rectangle(0, 180, 180, 0), GUI.style, playerFrame); jobList.Enabled = false; + + int i = 1; foreach (JobPrefab job in JobPrefab.List) { - GUITextBlock jobText = new GUITextBlock(new Rectangle(0,0,0,20), job.Name, GUI.style, Alignment.Left, Alignment.Right, jobList); + GUITextBlock jobText = new GUITextBlock(new Rectangle(0,0,0,20), i+". "+job.Name, GUI.style, Alignment.Left, Alignment.Right, jobList); jobText.UserData = job; GUIButton upButton = new GUIButton(new Rectangle(0, 0, 15, 15), "u", GUI.style, jobText); @@ -560,13 +562,17 @@ namespace Subsurface private void UpdateJobPreferences(GUIListBox listBox) { listBox.Deselect(); - for (int i = 1; i < listBox.children.Count; i++) + for (int i = 0; i < listBox.children.Count; i++) { float a = (float)(i - 1) / 3.0f; a = Math.Min(a, 3); Color color = new Color(1.0f - a, (1.0f - a) * 0.6f, 0.0f, 0.3f); listBox.children[i].Color = color; + listBox.children[i].HoverColor = color; + listBox.children[i].SelectedColor = color; + + (listBox.children[i] as GUITextBlock).Text = (i+1) + ". " + (listBox.children[i].UserData as JobPrefab).Name; } Game1.Client.SendCharacterData(); diff --git a/Subsurface/changelog.txt b/Subsurface/changelog.txt index e4c559a3a..28d44748c 100644 --- a/Subsurface/changelog.txt +++ b/Subsurface/changelog.txt @@ -1,4 +1,18 @@ +--------------------------------------------------------------------------------------------------------- +v0.1.3.2 +--------------------------------------------------------------------------------------------------------- + +Multiplayer: + - some major opimization to networked messages (less lag) + - option to disable UPnP port forwarding (which may have prevented some from hosting a server) + - a new round can't be started if a submarine hasn't been selected (which used to crash the game) + - maximum number of players can be changed + - fixed a bug in the net lobby screen that disabled the start button when the chat box was scrolled + to a specific position + - a window that displays some network statistics when hosting a server (can be activated by entering + "debugview" to the debug console) + --------------------------------------------------------------------------------------------------------- v0.1.3.1 --------------------------------------------------------------------------------------------------------- diff --git a/Subsurface/readme.txt b/Subsurface/readme.txt index 58e9c8186..d3d4f6d62 100644 --- a/Subsurface/readme.txt +++ b/Subsurface/readme.txt @@ -17,6 +17,31 @@ http://subsurface.gamepedia.com ------------------------------------------------------------------------ +Port forwarding: +You may try to forward ports on your router using UPnP (Universal Plug and +Play) port forwarding by selecting "Attempt UPnP port forwarding" in the +"Host Server" menu. + +However, UPnP isn't supported by all routers, so you may need to setup port +forwards manually. The exact steps for forwarding a port depend on your +router's model, but you may you may be able to find a port forwarding +guide for your particular router/application on portforward.com or by +practicing your google-fu skills. + +These are the values that you should use when forwarding a port to your +Subsurface server: + +Service/Application: subsurface +External Port: The port you have selected for your server (14242 by default) +Internal Port: The port you have selected for your server (14242 by default) +Protocol: UDP + + +------------------------------------------------------------------------ +------------------------------------------------------------------------ +Credits: +------------------------------------------------------------------------ + Programming, graphics, sounds, game design - Joonas Rikkonen ("Regalis") Graphics - James Bear ("Moonsaber99") diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index 081fd5b1c411bc1100c2264b927c91f9af91859b..12c9edcda02befc212f622bc558ef49b22379ef3 100644 GIT binary patch delta 11358 zcmeHN3tW{|wm=3h0Dho^`B}#z;3XhNDKZ z3p<#@N1iG@jZ#OJY)jlNnM^}aEe_}n_yU6f85RDDbTCr1y_F04A)p?33OEQn35*4L zpiUp7!$vtP4EQbRBA^O51Uzp57W@Kbi$PyQx*q7|80*=_ zqX3-UfD=e?Eb?r#U?V7Zz);9g)Y}hw9?$?f4CxC%HSm>D2CM=7D6k9I2HXTM8tG%e zFu>5vCZvA=UINNdUWhagnCZ9_eUEH7-XFItYSZx}u#D>dsPYKt$AM_j9e^A_a_ryG z#&-C2Eu)$7v`!7$z(!2?H0m_~6_0vl3;xAu|2*(EuoPGh#G4#}KEZ5)qrcDn!7IT~ zZ+C4&T7~>Nq(^{ElViJ2NX%-G1z=wU-T@MU-vP70e+3u^+K?z8>2=^R@F4h(>plSi z5{Oe^ya(J1dLL3xz!A|VKxk`n3~MvB+cV%-fY%P`H%OmEdI{-sfEjoVr~ww59EaNk zdmCdF8%+QOsB^6H>+A@<5>zqR_i+24^^zTE{lDcUhPXfW5`$-WiQz4O$4lU;#W3k= zNN=?Gqk~ERxx2{!C3kT=>Qz`_?R`vP+Z^9sy~X}q@$JCw61To2gfq!qFq!C$p@N@d zn#GqMepwjfcuTOkPf?nk3gXeNKd{aYS9M^;;J8Db14n>W4E!qBGu-Wt>lxEDTrcR? zyWZCr=YPoc{ES6rxZcY^lj~vZB1dfEIx=5Uf-APq%jv+a=U6A^cvI*p??RaT9W6h{ zy3@|D*krmKtAgSSKUc3-SnZvd&J0yO-pVj-99gkVy*aMLMpcAdu9KbdEVLqYPA$tH zg7n%=y&_~|9djv6a7rwchi+mh>Jq|364*^vVl^{?TgQpVn7kc=@8REhu`V&(>dER^ zzAdyQbR?5_s3+P~_7;b)j?sc** zYyqV@*>K4)I+ZzDP}n+@9RLQQo;TnIoJW2IuodV6Jj&>@lZA~>LfOZ_O27{=0bPLK z17XOvLAnh16to=(23-!s0|$UL;H^cv88`y^DBwj4h;?#%4Xg-Yw~3x17ATaMsFGM; z--+N=0?9x>U^?KMDhf3HiWtZPud}lr&KMC+=*F%yM^Oi;%PiEoJNt@RvJo|I((=CI z_eOV1`iVJaW1EL@n;trSPHNBF2MVG>);U6`mJg#n)7o%6YmTsqQTZ`BoQi^#P}=6B zS@`4-;UzKhAJl!Aa7Xv-YkJYiI)odF?~d7&0>7mACNo`eve17O)l>CxA(oyTrTg;n z;lf^b&aSar%+*(DTCFUb-iQ;YQ`rVRkgu8~?C{{$_l0^^IZp@?c!*U%)JPZ1PM3gS zF`CseHi_P_u>oYuRjvHNY+;u-Wxgf!b$%oH^TaA4m08aAoAPO_@5i0eKQH$E_Q|i4 zDK<$Q%pS_cK5#amSVpfZti zL(~wyVyEEJxivsOQx}RRQ{QC9zkgYfh5Rn)J&9Qvy;UmMC~GX+ zU*_O9QqaqLkixJL*s^w=3FLbDNkVoT;feDmWkKo z1(;eGEnhCKbe|7$f-zO&2 z#_c1;YUc761=na%aK3A7gPX+~X8t{DdpGxJqDvo1y)3c!KQW)=$`^BD&z+h#_O(^i zv6B$Ur_|!2=o*ITk0lV!hH(1?v0BZai+)C9Yjli#!k_9blEee2iDyOfep>6|m6CI4 z?(thMUq~z-*mvNK!*t}g(Chsx;EAqT5F$a`XL)7+eU6py$D|Y;$$smFy=Nvm)_mm4 zKS>ZHS!I&wV3wt+UoReAGHl2BZ->vg7NlA!b{6xasA*;oJEzx~Rb%+FU>EuOHJdKR!C{?`@E zlX12}yro;tjp^qz>iYaAf0I!10awiTQPRDl?I&EG6sMOl*9J@?Q4H0ZE|ahTMIg6& z8z`rd0}6qsfoFhcfvv!EfYIXjNJ{|2leQz>0lbdbx(n%Sq(+^du*^=-dvDh>>b?lN z6xauR2~+?E-*{e%{ zQsV%A4)_W<4}1+=0R9481ik?pfty>_3~_Dzzp9BhpnOBIzg81X`?zs@xxJ6$cwp?~ zatT<*F%gThX?^d9=9b=G-{~m3y}r{x-dW!%Z%QL*`3QufE`M zwBcEs#J!AL3&ncbStS-&4D%V-jao*^cb1BiS=07YB-VP0cZbr=mB zv6UYacZ=rvm~a}84EiM@2BMiszYYf)qmK?;F|c$nry%MY~A$E>gAYrYP`3;gTJVR!P;`P6=5> z8tl#o50#!V2fTgT_aJM_)A&JzOF{hb zI4PRZ(PYWWtqD?<8Zgf|_y6dzL}*@jcO_HEa4DHKjllUSYJ`-i5CqZqL6;QATm_Qg z+AT@0osvW$`;DGUrFs!8fiB-G^`?3cDV%ICNHt^`ENN8dE(I7>V1=emwpK|q8Lcgs zqiO1XX}j}#$;|8bNLJ>0)e!wohm2;vD|F*qBjw{bVnHF9eU#0W);Z~CQkSw|S%0fxz3=o4AsU&l@EK$Cn z63)Zp(V%xvNCiZD(Wa%DauV%cub@G9^A25#aZ#1d{K6U z^!KG2FUgm#U6m5~#Z@w{Fym@*g|VHMJt?WJYJNFRIVe(*Kd#mf-7f_2BtIfE{i%9Y%R12^4ifw(>cF zN}&fu|GTu8tZ&P4hJwjZFkDjKKy}e9m}~T`>@OQY@r%zJy-CfgBz2QqanJkH#g>vTL zv*dEMa*l%2X!;f@ji;w8`OwG?EuLz=lKS!$%M`qhxcrWULz^3# zhNsIXM920|CXwa1r0{7glp;nAYvdTJf5JFCX3B@!(u`ukhx=wL7np0K>_!4tqj=O> z;pORSl858WK8(6nZE(ly5ha=O zZm3~=MV<;pWQr_-CuFM!81+7**r<911RR~Ombam8UC>_HtzKi~6Q&Gu^vLk!BXiVB zMsbsr2p+LVJtmW-5%H%c0UjOrl6sc8Hsk1xJBG$JmwFX4ekBF-;3~BN0R|=T?U8)s z0k!fTs{TlZu}-Mvuv@Vjzyr$FbLjaa2`l-edP1a-jcQ*iJCAi>oh(~;a=p42{au#V z(#7?fKQF9Q=fF3gRVUH3Iq(R}M%BX88`L5%vQARNsA#xs=5~vQqyKS?Y<^aS4eHba z+_844(Nyq^iZJ>j2F|&xu5ITE(gc@TlN|NG)hM@Av+{5s?W9~H#A^Z<0<@bVWkjk$ zlyO4E8`5B{z9YpA)VA~O!?dY7Wn{ul>nF)UloX)_@Wgo<%n`j_4K=-V_W z)ieMfgBGL5>88aLJ5%n#qt0kYMY37cZj@A_Nj&Pjb`%N-)I-Qtf*ZGQEpFTfB^y12 z(xQ`ED1BF_ji8dlUPZ36k%Yu%jnTCVE?LW<|>+D5)n(3imRbv=xq4c1S}6unAa zPq|azFL?`C?QF7?yrQ&JzjE%jKchdY<(dx4cSZI!OmwQN0{ z6&h7$PMs`>E)Z1`JgFpa0F^hZC( z?rt7+*r5CSH(BcTk6{Gk>79br-TZ`CZvq`dd8_o* zEv9O`kn^c*=s*2WWMU9_(*zY35%& z0JIkn|I14C_8LBZL^AXoXXrU<#^PQrBWPaZM@(*AN-0D1cHVI<16Zmb0NsvCbM&RH zjTot*OW2eiTCGP(hIMJnYCXu`_?*@J1Xd@5FvN@hHAZ#M)<)hoc2`5ZjxE`rTA?F4 zHU7B)bH3ZxJlsd6%uOGan*Q)zvs5h)%*aelO)siVPA|p6x98|yKS=rWPSV!e(u03>1Zd+^a{Mj{;^{Wxrb z#@~z8W3azHrte`I&A(6ow4^#r7gz?pku8oS>r^?6cKEZ-+!?RWbEiWES~`^u)5kHV zBX;>?)Z-NMy+Vd|O1K zkf*c=o;Ff1U~CD+<_bwP?F)#|Z-73+nW$HjeV|@q`Q%1X$My|+)o=2)>@VN^;fbc# zc!)<2WAE9ZrO^ea9>gv`bz1B|Noy6WBd69$wx=}DdwzrPIv0U9)f9!TbupAO+jM)G z(xv`t+ooE6KdqHAgF$s|=)`*6=IDwGS^?=1-e2^6(QVgQb>s`vCU2beS`24&g!J*u zhmJg?cc(M+^?0(((+2UQNqQ7>TiO!F>2xGwc3GtU`M+Ygj(0tyo2huY-lJv7uUVoW z|L?|auWamltYz=Bw<4AWW-g3m-@$9FewB|njPAElG-Nubj%E*4dp8J!aNY(fMim150K->F^ zQSoQvopdMOMNls3_Fo_C3iXh7x)WQLaawZn8w>Q+BIPX5mv~$P)A;8=Bb`Qz(r^|m zr7ctOq3^wUy1%0?-+#dah|K9Y5)Z|-$2bs=LVhv+QV^$8P2r%jnj&~Q@>U#_TbD+F z^YiVJ@$Xz2sB1;*W~wX1@TCqZdU3O8Q;;`|XrxP;Ma#NNw7?#-v2hr~8@I7!oYO}& z-M5_z%|W+GOl4>219-HC&gXxDqQ^%T2T zh^6}BxD1aOErwgxecb2Eka)MEPexcD%%64flCi;2V}I43>}P~AP2Z<2(PQfN{N=A5 zb}wEOIQ-zK3LG5qsY_PSc`kKKka|@f(66}vza@TbiJx)BTU;N}6!8Cv*7(Uc!N2#V zu=1#W9x>umJ)9msjk`3ccSJt*GkqYVsN;H!;{~`w;ODrjCmqM@g&U1}2S)XO(!ZyL zSM`y!?c8px==?E#0)MqZ@5SiccluRc J-+=qoe*kWNiiQ9H delta 10867 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 mDBK#hIB{Lq6y;!+GSl+9@TKV!nqx=u2R(37`