From 51e68f0949cf0cd5567b00617fb26822dfe030f3 Mon Sep 17 00:00:00 2001 From: Regalis Date: Thu, 22 Oct 2015 01:04:42 +0300 Subject: [PATCH] Banning players, networkevent refactoring, wire syncing bugfixes, wrenches can be used as a melee weapon, proper error message for invalid IPs, drawing held items in correct position, fixed client crashing if sending a chatmessage while connection is lost --- Subsurface/Barotrauma.csproj | 1 + Subsurface/Content/Items/Tools/tools.xml | 10 +- Subsurface/Source/Characters/Character.cs | 41 ++--- .../Characters/HumanoidAnimController.cs | 8 +- Subsurface/Source/GameMain.cs | 3 +- Subsurface/Source/Items/CharacterInventory.cs | 68 ++++++++- .../Items/Components/Holdable/Holdable.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 2 +- .../Items/Components/Machines/Engine.cs | 14 +- .../Source/Items/Components/Machines/Pump.cs | 6 +- .../Source/Items/Components/Machines/Radar.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Machines/Steering.cs | 4 +- .../Items/Components/Power/PowerContainer.cs | 4 +- .../Items/Components/Signal/Connection.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 31 ++-- .../Source/Items/Components/Signal/Wire.cs | 12 +- Subsurface/Source/Items/Inventory.cs | 10 +- Subsurface/Source/Items/Item.cs | 36 +++-- Subsurface/Source/Networking/BanList.cs | 144 ++++++++++++++++++ Subsurface/Source/Networking/GameClient.cs | 48 +++--- Subsurface/Source/Networking/GameServer.cs | 109 +++++++++---- Subsurface/Source/Networking/NetStats.cs | 9 +- Subsurface/Source/Networking/NetworkEvent.cs | 62 ++++++-- Subsurface/Source/Networking/NetworkMember.cs | 24 ++- Subsurface/Source/Screens/MainMenuScreen.cs | 2 + Subsurface/Source/Screens/NetLobbyScreen.cs | 18 ++- Subsurface_Solution.v12.suo | Bin 829952 -> 784896 bytes 28 files changed, 520 insertions(+), 154 deletions(-) create mode 100644 Subsurface/Source/Networking/BanList.cs diff --git a/Subsurface/Barotrauma.csproj b/Subsurface/Barotrauma.csproj index b825317a0..23696c51d 100644 --- a/Subsurface/Barotrauma.csproj +++ b/Subsurface/Barotrauma.csproj @@ -98,6 +98,7 @@ + diff --git a/Subsurface/Content/Items/Tools/tools.xml b/Subsurface/Content/Items/Tools/tools.xml index 619f92ab2..80e3f5f3d 100644 --- a/Subsurface/Content/Items/Tools/tools.xml +++ b/Subsurface/Content/Items/Tools/tools.xml @@ -56,7 +56,7 @@ - @@ -110,7 +110,7 @@ + holdangle="30" handle1="-5,0"/> @@ -124,8 +124,10 @@ - + + + diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index 3248a7289..d310dfcab 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -405,7 +405,7 @@ namespace Barotrauma if (Info.PickedItemIDs.Any()) { - foreach (int id in Info.PickedItemIDs) + foreach (ushort id in Info.PickedItemIDs) { Item item = FindEntityByID(id) as Item; if (item == null) continue; @@ -627,7 +627,7 @@ namespace Barotrauma selectedCharacter = null; if (createNetworkEvent) - new NetworkEvent(NetworkEventType.SelectCharacter, ID, true, -1); + new NetworkEvent(NetworkEventType.SelectCharacter, ID, true, 0); } /// @@ -1075,7 +1075,7 @@ namespace Barotrauma if (GameMain.Server != null) { - new NetworkEvent(NetworkEventType.KillCharacter, ID, false); + new NetworkEvent(NetworkEventType.KillCharacter, ID, false, true); } if (GameMain.GameSession != null) @@ -1088,12 +1088,12 @@ namespace Barotrauma { if (type == NetworkEventType.PickItem) { - message.Write((int)data); + message.Write((ushort)data); return true; } else if (type== NetworkEventType.SelectCharacter) { - message.Write((int)data); + message.Write((ushort)data); return true; } else if (type == NetworkEventType.KillCharacter) @@ -1115,10 +1115,10 @@ namespace Barotrauma GetInputState(InputType.SecondaryHeld)) || LargeUpdateTimer <= 0; message.Write(hasInputs); + message.Write((float)NetTime.Now); if (!hasInputs) return true; - message.Write((float)NetTime.Now); // Write byte = move direction //message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.X, -10.0f, 10.0f), -10.0f, 10.0f, 8); @@ -1168,7 +1168,7 @@ namespace Barotrauma message.WriteRangedSingle(MathHelper.Clamp(AnimController.StunTimer,0.0f,60.0f), 0.0f, 60.0f, 8); message.Write((byte)((health/maxHealth)*255.0f)); - LargeUpdateTimer = 50; + LargeUpdateTimer = 30; } else { @@ -1189,16 +1189,9 @@ namespace Barotrauma { System.Diagnostics.Debug.WriteLine("**************** PickItem networkevent received"); - int itemId = -1; + ushort itemId = 0; - try - { - itemId = message.ReadInt32(); - } - catch - { - return; - } + itemId = message.ReadUInt16(); System.Diagnostics.Debug.WriteLine("item id: "+itemId); @@ -1212,8 +1205,8 @@ namespace Barotrauma } else if (type == NetworkEventType.SelectCharacter) { - int characterId = message.ReadInt32(); - if (characterId==-1) + ushort characterId = message.ReadUInt16(); + if (characterId==0) { DeselectCharacter(false); } @@ -1254,14 +1247,14 @@ namespace Barotrauma try { bool hasInputs = message.ReadBoolean(); - if (!hasInputs) + sendingTime = message.ReadFloat(); + + if (!hasInputs && sendingTime > LastNetworkUpdate) { ClearInputs(); return; } - sendingTime = message.ReadFloat(); - actionKeyState = message.ReadBoolean(); secondaryKeyState = message.ReadBoolean(); @@ -1321,7 +1314,7 @@ namespace Barotrauma } else { - cursorPos = Position + new Vector2(1000.0f, 0.0f) * dir; + cursorPosition = Position + new Vector2(1000.0f, 0.0f) * dir; } if (isLargeUpdate) @@ -1383,9 +1376,7 @@ namespace Barotrauma catch { return; } - Limb torso = AnimController.GetLimb(LimbType.Torso); - if (torso == null) torso = AnimController.GetLimb(LimbType.Head); - torso.body.TargetPosition = pos; + AnimController.RefLimb.body.TargetPosition = pos; LargeUpdateTimer = 0; } diff --git a/Subsurface/Source/Characters/HumanoidAnimController.cs b/Subsurface/Source/Characters/HumanoidAnimController.cs index 23b3e9c09..66055a29d 100644 --- a/Subsurface/Source/Characters/HumanoidAnimController.cs +++ b/Subsurface/Source/Characters/HumanoidAnimController.cs @@ -865,7 +865,13 @@ namespace Barotrauma Vector2 bodyVelocity = torso.body.LinearVelocity / 60.0f; item.body.ResetDynamics(); - item.SetTransform(MathUtils.SmoothStep(item.body.SimPosition, transformedHoldPos + bodyVelocity, 0.5f), itemAngle); + + Vector2 currItemPos = (character.SelectedItems[0]==item) ? + rightHand.pullJoint.WorldAnchorA - transformedHandlePos[0] : + leftHand.pullJoint.WorldAnchorA - transformedHandlePos[1]; + item.SetTransform(currItemPos, itemAngle); + + //item.SetTransform(MathUtils.SmoothStep(item.body.SimPosition, transformedHoldPos + bodyVelocity, 0.5f), itemAngle); for (int i = 0; i < 2; i++) { diff --git a/Subsurface/Source/GameMain.cs b/Subsurface/Source/GameMain.cs index 33943dfc7..95655eae0 100644 --- a/Subsurface/Source/GameMain.cs +++ b/Subsurface/Source/GameMain.cs @@ -218,8 +218,7 @@ namespace Barotrauma MainMenuScreen = new MainMenuScreen(this); LobbyScreen = new LobbyScreen(); - - NetLobbyScreen = new NetLobbyScreen(); + ServerListScreen = new ServerListScreen(); EditMapScreen = new EditMapScreen(); diff --git a/Subsurface/Source/Items/CharacterInventory.cs b/Subsurface/Source/Items/CharacterInventory.cs index c1b470c64..396aec3ed 100644 --- a/Subsurface/Source/Items/CharacterInventory.cs +++ b/Subsurface/Source/Items/CharacterInventory.cs @@ -161,9 +161,9 @@ namespace Barotrauma { //PutItem(items[i], i, false, false); Inventory otherInventory = items[i].inventory; - if (otherInventory!=null) + if (otherInventory!=null && createNetworkEvent) { - new Networking.NetworkEvent(Networking.NetworkEventType.InventoryUpdate, otherInventory.Owner.ID, true); + new Networking.NetworkEvent(Networking.NetworkEventType.InventoryUpdate, otherInventory.Owner.ID, true, true); } combined = true; @@ -263,17 +263,77 @@ namespace Barotrauma SpriteEffects.None, 0.1f); } } - + for (int i = 0; i < capacity; i++) { slotRect.X = (int)slotPositions[i].X; slotRect.Y = (int)slotPositions[i].Y; + bool multiSlot = false; + //skip if the item is in multiple slots + if (items[i]!=null) + { + for (int n = 0; n < capacity; n++ ) + { + if (i==n || items[n] != items[i]) continue; + multiSlot = true; + break; + } + } - UpdateSlot(spriteBatch, slotRect, i, items[i], i>4); + if (multiSlot) continue; + + UpdateSlot(spriteBatch, slotRect, i, items[i], i > 4); + if (draggingItem!=null && draggingItem == items[i]) draggingItemSlot = slotRect; } + + for (int i = 0; i < capacity; i++) + { + + //Rectangle multiSlotRect = Rectangle.Empty; + bool multiSlot = false; + + //check if the item is in multiple slots + if (items[i] != null) + { + slotRect.X = (int)slotPositions[i].X; + slotRect.Y = (int)slotPositions[i].Y; + slotRect.Width = 40; + slotRect.Height = 40; + + for (int n = 0; n < capacity; n++) + { + if (items[n] != items[i]) continue; + + if (!multiSlot && i > n) break; + + if (i!=n) + { + multiSlot = true; + slotRect = Rectangle.Union( + new Rectangle((int)slotPositions[n].X, (int)slotPositions[n].Y, rectWidth, rectHeight), slotRect); + } + } + } + + if (!multiSlot) continue; + + UpdateSlot(spriteBatch, slotRect, i, items[i], i > 4); + + //if (multiSlot && i == first) + //{ + // multiSlotPos = multiSlotPos / count; + // items[i].Sprite.Draw(spriteBatch, new Vector2(multiSlotPos.X + rectWidth / 2, multiSlotPos.Y + rectHeight / 2), items[i].Color); + //} + + } + + slotRect.Width = rectWidth; + slotRect.Height = rectHeight; + + if (draggingItem != null && !draggingItemSlot.Contains(PlayerInput.MousePosition)) { if (PlayerInput.GetMouseState.LeftButton == ButtonState.Pressed) diff --git a/Subsurface/Source/Items/Components/Holdable/Holdable.cs b/Subsurface/Source/Items/Components/Holdable/Holdable.cs index 9d0f8537f..99d1c7886 100644 --- a/Subsurface/Source/Items/Components/Holdable/Holdable.cs +++ b/Subsurface/Source/Items/Components/Holdable/Holdable.cs @@ -215,7 +215,7 @@ namespace Barotrauma.Items.Components attached = true; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, true); return true; } diff --git a/Subsurface/Source/Items/Components/Holdable/MeleeWeapon.cs b/Subsurface/Source/Items/Components/Holdable/MeleeWeapon.cs index 5c474d137..7fe6922b1 100644 --- a/Subsurface/Source/Items/Components/Holdable/MeleeWeapon.cs +++ b/Subsurface/Source/Items/Components/Holdable/MeleeWeapon.cs @@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components } else { - ac.HoldItem(deltaTime, item, handlePos, new Vector2(hitPos, 0.0f), aimPos, false, 0.0f); + ac.HoldItem(deltaTime, item, handlePos, new Vector2(hitPos, 0.0f), aimPos, false, holdAngle); } } else diff --git a/Subsurface/Source/Items/Components/Machines/Engine.cs b/Subsurface/Source/Items/Components/Machines/Engine.cs index d41bb4bc4..b940fe3e1 100644 --- a/Subsurface/Source/Items/Components/Machines/Engine.cs +++ b/Subsurface/Source/Items/Components/Machines/Engine.cs @@ -93,10 +93,18 @@ namespace Barotrauma.Items.Components spriteBatch.DrawString(GUI.Font, "Force: " + (int)(targetForce) + " %", new Vector2(GuiFrame.Rect.X + 30, GuiFrame.Rect.Y + 30), Color.White); - if (GUI.DrawButton(spriteBatch, new Rectangle(GuiFrame.Rect.X + 280, GuiFrame.Rect.Y + 30, 40, 40), "+", true)) targetForce += 1.0f; - if (GUI.DrawButton(spriteBatch, new Rectangle(GuiFrame.Rect.X + 280, GuiFrame.Rect.Y + 80, 40, 40), "-", true)) targetForce -= 1.0f; + if (GUI.DrawButton(spriteBatch, new Rectangle(GuiFrame.Rect.X + 280, GuiFrame.Rect.Y + 30, 40, 40), "+", true)) + { + targetForce += 1.0f; + item.NewComponentEvent(this, true, false); + } + if (GUI.DrawButton(spriteBatch, new Rectangle(GuiFrame.Rect.X + 280, GuiFrame.Rect.Y + 80, 40, 40), "-", true)) + { + targetForce -= 1.0f; + item.NewComponentEvent(this, true, false); + } + - item.NewComponentEvent(this, true); } public override void UpdateBroken(float deltaTime, Camera cam) diff --git a/Subsurface/Source/Items/Components/Machines/Pump.cs b/Subsurface/Source/Items/Components/Machines/Pump.cs index 16b6b2e14..2efdeb34d 100644 --- a/Subsurface/Source/Items/Components/Machines/Pump.cs +++ b/Subsurface/Source/Items/Components/Machines/Pump.cs @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components targetLevel = null; IsActive = !IsActive; if (!IsActive) currPowerConsumption = 0.0f; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, true); } spriteBatch.DrawString(GUI.Font, "Pumping speed: " + (int)flowPercentage + " %", new Vector2(x + 20, y + 80), Color.White); @@ -125,12 +125,12 @@ namespace Barotrauma.Items.Components if (GUI.DrawButton(spriteBatch, new Rectangle(x + 200, y + 70, 40, 40), "OUT", false)) { FlowPercentage -= 10.0f; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); } if (GUI.DrawButton(spriteBatch, new Rectangle(x + 250, y + 70, 40, 40), "IN", false)) { FlowPercentage += 10.0f; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); } } diff --git a/Subsurface/Source/Items/Components/Machines/Radar.cs b/Subsurface/Source/Items/Components/Machines/Radar.cs index 5f1ceb455..0df23e5c4 100644 --- a/Subsurface/Source/Items/Components/Machines/Radar.cs +++ b/Subsurface/Source/Items/Components/Machines/Radar.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components if (GUI.DrawButton(spriteBatch, new Rectangle(x + 20, y + 20, 200, 30), "Activate Radar")) { IsActive = !IsActive; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); } int radius = GuiFrame.Rect.Height / 2 - 10; diff --git a/Subsurface/Source/Items/Components/Machines/Reactor.cs b/Subsurface/Source/Items/Components/Machines/Reactor.cs index d6d03a6bb..d34334680 100644 --- a/Subsurface/Source/Items/Components/Machines/Reactor.cs +++ b/Subsurface/Source/Items/Components/Machines/Reactor.cs @@ -371,7 +371,7 @@ namespace Barotrauma.Items.Components if (valueChanged) { - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); valueChanged = false; } } diff --git a/Subsurface/Source/Items/Components/Machines/Steering.cs b/Subsurface/Source/Items/Components/Machines/Steering.cs index 4fc27995a..2fb7040e4 100644 --- a/Subsurface/Source/Items/Components/Machines/Steering.cs +++ b/Subsurface/Source/Items/Components/Machines/Steering.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components networkUpdateTimer -= deltaTime; if (networkUpdateTimer<=0.0f) { - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); networkUpdateTimer = 1.0f; valueChanged = false; } @@ -124,7 +124,7 @@ namespace Barotrauma.Items.Components { AutoPilot = !AutoPilot; - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, true); } GUI.DrawLine(spriteBatch, diff --git a/Subsurface/Source/Items/Components/Power/PowerContainer.cs b/Subsurface/Source/Items/Components/Power/PowerContainer.cs index 170ca3e8c..12f5bebf5 100644 --- a/Subsurface/Source/Items/Components/Power/PowerContainer.cs +++ b/Subsurface/Source/Items/Components/Power/PowerContainer.cs @@ -186,13 +186,13 @@ namespace Barotrauma.Items.Components if (GUI.DrawButton(spriteBatch, new Rectangle(x + 200, y + 90, 40, 40), "+")) { rechargeSpeed = Math.Min(rechargeSpeed + maxRechargeSpeed*0.1f, maxRechargeSpeed); - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); } if (GUI.DrawButton(spriteBatch, new Rectangle(x + 250, y + 90, 40, 40), "-")) { rechargeSpeed = Math.Max(rechargeSpeed - maxRechargeSpeed * 0.1f, 0.0f); - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, false); } } diff --git a/Subsurface/Source/Items/Components/Signal/Connection.cs b/Subsurface/Source/Items/Components/Signal/Connection.cs index 16d91ed27..2d5b64e96 100644 --- a/Subsurface/Source/Items/Components/Signal/Connection.cs +++ b/Subsurface/Source/Items/Components/Signal/Connection.cs @@ -280,7 +280,7 @@ namespace Barotrauma.Items.Components { if (!PlayerInput.LeftButtonDown()) { - panel.Item.NewComponentEvent(panel, true); + panel.Item.NewComponentEvent(panel, true, true); draggingConnected = null; } } diff --git a/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs b/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs index 478e2681d..2f4ff91f2 100644 --- a/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs @@ -125,10 +125,9 @@ namespace Barotrauma.Items.Components { Wire[] wires = Array.FindAll(c.Wires, w => w != null); message.Write((byte)wires.Length); - for (int i = 0 ; i < c.Wires.Length; i++) + for (int i = 0 ; i < wires.Length; i++) { - if (c.Wires[i] == null) continue; - message.Write(c.Wires[i].Item.ID); + message.Write(wires[i].Item.ID); } } } @@ -140,26 +139,22 @@ namespace Barotrauma.Items.Components { //int wireCount = c.Wires.Length; c.ClearConnections(); - try + + byte wireCount = message.ReadByte(); + + for (int i = 0; i < wireCount; i++) { - byte wireCount = message.ReadByte(); - - for (int i = 0; i < wireCount; i++) - { - ushort wireId = message.ReadUInt16(); + ushort wireId = message.ReadUInt16(); - Item wireItem = MapEntity.FindEntityByID(wireId) as Item; - if (wireItem == null) continue; + Item wireItem = MapEntity.FindEntityByID(wireId) as Item; + if (wireItem == null) continue; - Wire wireComponent = wireItem.GetComponent(); - if (wireComponent == null) continue; + Wire wireComponent = wireItem.GetComponent(); + if (wireComponent == null) continue; - c.Wires[i] = wireComponent; - wireComponent.Connect(c, false); - } + c.Wires[i] = wireComponent; + wireComponent.Connect(c, false); } - - catch { } } } } diff --git a/Subsurface/Source/Items/Components/Signal/Wire.cs b/Subsurface/Source/Items/Components/Signal/Wire.cs index 7c6b3871b..e705e6c0a 100644 --- a/Subsurface/Source/Items/Components/Signal/Wire.cs +++ b/Subsurface/Source/Items/Components/Signal/Wire.cs @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components CleanNodes(); } - if (!loading) Item.NewComponentEvent(this, true); + if (!loading) Item.NewComponentEvent(this, true, true); } public override void Equip(Character character) @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components if (Nodes.Count > 1) { Nodes.RemoveAt(Nodes.Count - 1); - item.NewComponentEvent(this, true); + item.NewComponentEvent(this, true, true); } } @@ -409,8 +409,8 @@ namespace Barotrauma.Items.Components public override void FillNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetOutgoingMessage message) { - message.Write(Nodes.Count); - for (int i = 0; i < Nodes.Count; i++) + message.Write((byte)Math.Min(Nodes.Count, 10)); + for (int i = 0; i < Math.Min(Nodes.Count,10); i++) { message.Write(Nodes[i].X); message.Write(Nodes[i].Y); @@ -420,8 +420,8 @@ namespace Barotrauma.Items.Components public override void ReadNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetIncomingMessage message) { Nodes.Clear(); - int nodeCount = message.ReadInt32(); - for (int i = 0; i < nodeCount; i++) + int nodeCount = message.ReadByte(); + for (int i = 0; i(); + if (holdable!=null && holdable.Picker !=null) + { + float depth = Sprite.Depth; + if (holdable.Picker.SelectedItems[0]==this) + { + depth = holdable.Picker.AnimController.GetLimb(LimbType.RightHand).sprite.Depth + 0.000001f; + } + else if (holdable.Picker.SelectedItems[1] == this) + { + depth = holdable.Picker.AnimController.GetLimb(LimbType.LeftArm).sprite.Depth - 0.000001f; + } + + body.Draw(spriteBatch, prefab.sprite, color, depth); + } + else + { + body.Draw(spriteBatch, prefab.sprite, color); + } } } @@ -1149,11 +1167,12 @@ namespace Barotrauma } - public void NewComponentEvent(ItemComponent ic, bool isClient) + public void NewComponentEvent(ItemComponent ic, bool isClient, bool isImportant) { int index = components.IndexOf(ic); - new NetworkEvent(NetworkEventType.UpdateComponent, ID, isClient, index); + new NetworkEvent(isImportant ? + NetworkEventType.ImportantComponentUpdate : NetworkEventType.ComponentUpdate, ID, isClient, index); } public override bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) @@ -1173,7 +1192,8 @@ namespace Barotrauma var itemContainer = GetComponent(); if (itemContainer == null || itemContainer.inventory == null) return false; return itemContainer.inventory.FillNetworkData(NetworkEventType.InventoryUpdate, message, data); - case NetworkEventType.UpdateComponent: + case NetworkEventType.ComponentUpdate: + case NetworkEventType.ImportantComponentUpdate: int componentIndex = (int)data; if (componentIndex < 0 || componentIndex >= components.Count) return false; @@ -1230,10 +1250,7 @@ namespace Barotrauma { case NetworkEventType.DropItem: Vector2 newSimPos = Vector2.Zero; - if (body != null) - { - newSimPos = new Vector2(message.ReadFloat(), message.ReadFloat()); - } + newSimPos = new Vector2(message.ReadFloat(), message.ReadFloat()); SetTransform(newSimPos, body.Rotation); Drop(null, false); break; @@ -1242,7 +1259,8 @@ namespace Barotrauma if (itemContainer == null || itemContainer.inventory == null) return; itemContainer.inventory.ReadNetworkData(NetworkEventType.DropItem, message); break; - case NetworkEventType.UpdateComponent: + case NetworkEventType.ComponentUpdate: + case NetworkEventType.ImportantComponentUpdate: int componentIndex = message.ReadByte(); if (componentIndex < 0 || componentIndex > components.Count - 1) return; components[componentIndex].ReadNetworkData(type, message); diff --git a/Subsurface/Source/Networking/BanList.cs b/Subsurface/Source/Networking/BanList.cs new file mode 100644 index 000000000..e3d64ad26 --- /dev/null +++ b/Subsurface/Source/Networking/BanList.cs @@ -0,0 +1,144 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Barotrauma.Networking +{ + class BanList + { + const string SavePath = "Data/bannedplayers.xml"; + + private List bannedPlayers; + + private GUIFrame banFrame; + + public GUIFrame BanFrame + { + get { return banFrame; } + } + + public BanList() + { + bannedPlayers = new List(); + + if (File.Exists(SavePath)) + { + string[] lines; + try + { + lines = File.ReadAllLines(SavePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to open the list of banned players in "+SavePath, e); + return; + } + + foreach (string line in lines) + { + string[] separatedLine = line.Split(','); + if (separatedLine.Length != 2) continue; + + bannedPlayers.Add(new BannedPlayer(separatedLine[0],separatedLine[1])); + } + } + } + + public void BanPlayer(string name, string ip) + { + if (bannedPlayers.FirstOrDefault(bp => bp.IP == ip)!=null) return; + + bannedPlayers.Add(new BannedPlayer(name,ip)); + } + + public bool IsBanned(string IP) + { + return bannedPlayers.FirstOrDefault(bp => bp.IP == IP)!=null; + } + + private GUIFrame CreateBanFrame() + { + banFrame = new GUIFrame(new Rectangle(0,0,GameMain.GraphicsWidth,GameMain.GraphicsHeight), Color.Black*0.3f); + + GUIFrame innerFrame = new GUIFrame(new Rectangle(0,0,300,300), null, Alignment.Center, GUI.Style, banFrame); + + new GUITextBlock(new Rectangle(0, 0, 0, 30), "Banned IPs:", GUI.Style, Alignment.Left, Alignment.Left, innerFrame, false, GUI.LargeFont); + var banList = new GUIListBox(new Rectangle(0, 30, 200, 0), GUI.Style, innerFrame); + + foreach (BannedPlayer bannedPlayer in bannedPlayers) + { + GUITextBlock textBlock = new GUITextBlock( + new Rectangle(0, 0, 0, 25), + bannedPlayer.IP+" ("+bannedPlayer.Name+")", + GUI.Style, + Alignment.Left, Alignment.Left, banList); + textBlock.Padding = new Vector4(10.0f, 0.0f, 0.0f, 0.0f); + textBlock.UserData = banList; + + var removeButton = new GUIButton(new Rectangle(0,0,100,20), "Remove", Alignment.Right, GUI.Style, textBlock); + removeButton.UserData = bannedPlayer; + removeButton.OnClicked = RemoveBan; + } + + var closeButton = new GUIButton(new Rectangle(0,0,100,20), "Close", Alignment.BottomRight, GUI.Style, innerFrame); + closeButton.OnClicked = CloseFrame; + + + return banFrame; + } + + private bool RemoveBan(GUIButton button, object obj) + { + BannedPlayer banned = obj as BannedPlayer; + if (banned == null) return false; + + bannedPlayers.Remove(banned); + + CreateBanFrame(); + + return true; + } + + private bool CloseFrame(GUIButton button, object obj) + { + banFrame = null; + + return true; + } + + public void Save() + { + List lines = new List(); + + foreach (BannedPlayer banned in bannedPlayers) + { + lines.Add(banned.Name + "," + banned.IP); + } + + try + { + File.WriteAllLines(SavePath, lines); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving the list of banned players to "+SavePath+" failed", e); + } + } + + } + + class BannedPlayer + { + public string Name; + public string IP; + + public BannedPlayer(string name, string ip) + { + this.Name = name; + this.IP = ip; + } + } +} diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index d919ab677..ca6d51ca0 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Networking otherClients = new List(); - + GameMain.NetLobbyScreen = new NetLobbyScreen(); } public void ConnectToServer(string hostIP, string password = "") @@ -94,7 +94,7 @@ namespace Barotrauma.Networking { IPEndPoint = new System.Net.IPEndPoint(NetUtility.Resolve(serverIP), Port); } - catch (ArgumentNullException e) + catch (Exception e) { new GUIMessageBox("Could not connect to server", "Failed to resolve address ''"+serverIP+":"+Port+"''. Please make sure you have entered a valid IP address."); return; @@ -111,7 +111,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Couldn't connect to "+hostIP+". Error message: "+e.Message); Disconnect(); - GameMain.NetLobbyScreen.Select(); + GameMain.ServerListScreen.Select(); return; } @@ -291,7 +291,11 @@ namespace Barotrauma.Networking } else { - if (Screen.Selected != GameMain.GameScreen) GameMain.NetLobbyScreen.Select(); + if (Screen.Selected != GameMain.GameScreen) + { + GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.NetLobbyScreen.Select(); + } connected = true; } @@ -347,7 +351,7 @@ namespace Barotrauma.Networking } else if (gameStarted) { - myCharacter.SendNetworkEvent(true); + new NetworkEvent(myCharacter.ID, true); } } @@ -374,21 +378,12 @@ namespace Barotrauma.Networking client.SendMessage(message, NetDeliveryMethod.Unreliable); } } - - } NetworkEvent.events.Clear(); - - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.B)) - { - SendChatMessage("asdfsdaf"); - } - + // Update current time - updateTimer = DateTime.Now + updateInterval; - - + updateTimer = DateTime.Now + updateInterval; } /// @@ -440,13 +435,24 @@ namespace Barotrauma.Networking AddChatMessage(inc.ReadString(), ChatMessageType.Server); Client disconnectedClient = otherClients.Find(c => c.ID == leavingID); if (disconnectedClient != null) GameMain.NetLobbyScreen.RemovePlayer(disconnectedClient.name); + + if (!gameStarted) return; + + List crew = new List(); + foreach (Character c in Character.CharacterList) + { + if (!c.IsNetworkPlayer || !c.IsHumanoid || c.Info==null) continue; + crew.Add(c); + } + + CreateCrewFrame(crew); break; case (byte)PacketTypes.KickedOut: string msg = inc.ReadString(); - new GUIMessageBox("You have been kicked out from the server", msg); + new GUIMessageBox("Disconnected from server", msg); Disconnect(); GameMain.MainMenuScreen.Select(); @@ -588,6 +594,10 @@ namespace Barotrauma.Networking if (GameMain.GameSession!=null) GameMain.GameSession.EndShift(""); myCharacter = null; + foreach (Client c in otherClients) + { + c.character = null; + } yield return CoroutineStatus.Success; @@ -698,6 +708,8 @@ namespace Barotrauma.Networking { //AddChatMessage(message); + if (client.ServerConnection == null) return; + type = (gameStarted && myCharacter != null && myCharacter.IsDead) ? ChatMessageType.Dead : ChatMessageType.Default; ReliableMessage msg = reliableChannel.CreateMessage(); @@ -729,7 +741,7 @@ namespace Barotrauma.Networking break; case 2: msg.Write((byte)PacketTypes.NetworkEvent); - msg.Write((byte)NetworkEventType.UpdateComponent); + msg.Write((byte)NetworkEventType.ComponentUpdate); msg.Write((int)Item.itemList[Rand.Int(Item.itemList.Count)].ID); msg.Write(Rand.Int(8)); break; diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 16ec29e2d..dc89bc1b4 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -31,6 +31,8 @@ namespace Barotrauma.Networking private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 40); private DateTime refreshMasterTimer; + + private BanList banList; private bool masterServerResponded; @@ -54,9 +56,11 @@ namespace Barotrauma.Networking public GameServer(string name, int port, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10) { - var endRoundButton = new GUIButton(new Rectangle(GameMain.GraphicsWidth - 290, 20, 150, 25), "End round", Alignment.TopLeft, GUI.Style, inGameHUD); + var endRoundButton = new GUIButton(new Rectangle(GameMain.GraphicsWidth - 170, 20, 150, 25), "End round", Alignment.TopLeft, GUI.Style, inGameHUD); endRoundButton.OnClicked = EndButtonHit; + banList = new BanList(); + this.name = name; this.password = password; @@ -297,7 +301,7 @@ namespace Barotrauma.Networking { if (gameStarted) { - if (myCharacter != null) myCharacter.SendNetworkEvent(true); + if (myCharacter != null) new NetworkEvent(myCharacter.ID, true); foreach (Character c in Character.CharacterList) { @@ -305,7 +309,7 @@ namespace Barotrauma.Networking if (c.SimPosition == Vector2.Zero || c.SimPosition.Length() < 100.0f) { - c.SendNetworkEvent(false); + new NetworkEvent(c.ID, false); } } } @@ -457,14 +461,6 @@ namespace Barotrauma.Networking List recipients = connectedClients.FindAll(c => c.Connection != inc.SenderConnection && c.inGame); if (recipients.Count == 0) break; - - //foreach (Client client in connectedClients) - //{ - // if (client.Connection == inc.SenderConnection) continue; - // if (!client.inGame) continue; - - // recipients.Add(client.Connection); - //} if (isReliable) { @@ -524,6 +520,13 @@ namespace Barotrauma.Networking if (inc.ReadByte() != (byte)PacketTypes.Login) return; DebugConsole.NewMessage("New player has joined the server", Color.White); + + if (banList.IsBanned(inc.SenderEndPoint.Address.ToString())) + { + inc.SenderConnection.Deny("You have been banned from the server"); + DebugConsole.NewMessage("Banned player tried to join the server", Color.Red); + return; + } if (connectedClients.Find(c => c.Connection == inc.SenderConnection)!=null) { @@ -595,6 +598,8 @@ namespace Barotrauma.Networking { disconnectedClients.Remove(existingClient); connectedClients.Add(existingClient); + + UpdateCrewFrame(); } } if (existingClient != null) @@ -617,6 +622,8 @@ namespace Barotrauma.Networking connectedClients.Add(newClient); + UpdateCrewFrame(); + inc.SenderConnection.Approve(); } @@ -737,7 +744,6 @@ namespace Barotrauma.Networking characterInfos.Add(characterInfo); } - List crew = new List(); WayPoint[] assignedWayPoints = WayPoint.SelectCrewSpawnPoints(characterInfos); for (int i = 0; i < connectedClients.Count; i++) @@ -745,8 +751,6 @@ namespace Barotrauma.Networking connectedClients[i].character = new Character( connectedClients[i].characterInfo, assignedWayPoints[i], true); connectedClients[i].character.GiveJobItems(assignedWayPoints[i]); - - crew.Add(connectedClients[i].character); } if (characterInfo != null) @@ -755,8 +759,6 @@ namespace Barotrauma.Networking Character.Controlled = myCharacter; myCharacter.GiveJobItems(assignedWayPoints[assignedWayPoints.Length - 1]); - - crew.Add(myCharacter); } yield return CoroutineStatus.Running; @@ -789,8 +791,8 @@ namespace Barotrauma.Networking } SendMessage(msg, NetDeliveryMethod.ReliableUnordered, null); - - CreateCrewFrame(crew); + + UpdateCrewFrame(); //give some time for the clients to load the map yield return new WaitForSeconds(2.0f); @@ -907,23 +909,51 @@ namespace Barotrauma.Networking } AddChatMessage(msg, ChatMessageType.Server); + + UpdateCrewFrame(); } - public void KickPlayer(string playerName) + private void UpdateCrewFrame() { - playerName = playerName.ToLower(); + List crew = new List(); + foreach (Client c in connectedClients) { - if (c.name.ToLower() == playerName) KickClient(c); - break; + if (c.character == null || !c.inGame) continue; + + crew.Add(c.character); } + + if (myCharacter != null) crew.Add(myCharacter); + + CreateCrewFrame(crew); } - private void KickClient(Client client) + public void KickPlayer(string playerName, bool ban = false) + { + playerName = playerName.ToLower(); + + Client client = connectedClients.Find( c => c.name.ToLower() == playerName || + (c.character != null && c.character.Name.ToLower() == playerName)); + + if (client == null) return; + + KickClient(client, ban); + } + + private void KickClient(Client client, bool ban = false) { if (client == null) return; - DisconnectClient(client, client.name + " has been kicked from the server", "You have been kicked from the server"); + if (ban) + { + DisconnectClient(client, client.name + " has been banned from the server", "You have been banned from the server"); + banList.BanPlayer(client.name, client.Connection.RemoteEndPoint.Address.ToString()); + } + else + { + DisconnectClient(client, client.name + " has been kicked from the server", "You have been kicked from the server"); + } } public void NewTraitor(Client traitor, Client target) @@ -999,6 +1029,32 @@ namespace Barotrauma.Networking return true; } + protected override bool SelectCrewCharacter(GUIComponent component, object obj) + { + base.SelectCrewCharacter(component, obj); + + var characterFrame = crewFrame.FindChild("selectedcharacter"); + + Character character = obj as Character; + if (obj == null) return false; + + if (character != myCharacter) + { + var kickButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Kick", Alignment.BottomLeft, GUI.Style, characterFrame); + kickButton.UserData = character.Name; + kickButton.OnClicked += GameMain.NetLobbyScreen.KickPlayer; + + var banButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Ban", Alignment.BottomRight, GUI.Style, characterFrame); + banButton.UserData = character.Name; + banButton.OnClicked += GameMain.NetLobbyScreen.BanPlayer; + } + + + + return true; + + } + public override void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Server) { AddChatMessage(message, type); @@ -1081,7 +1137,7 @@ namespace Barotrauma.Networking private void AssignJobs() { List unassigned = new List(connectedClients); - + int[] assignedClientCount = new int[JobPrefab.List.Count]; if (characterInfo!=null) @@ -1201,7 +1257,7 @@ namespace Barotrauma.Networking break; case 1: msg.Write((byte)PacketTypes.NetworkEvent); - msg.Write((byte)NetworkEventType.UpdateComponent); + msg.Write((byte)NetworkEventType.ComponentUpdate); msg.Write((int)Item.itemList[Rand.Int(Item.itemList.Count)].ID); msg.Write(Rand.Int(8)); break; @@ -1224,6 +1280,7 @@ namespace Barotrauma.Networking public override void Disconnect() { + banList.Save(); server.Shutdown("The server has shut down"); } } diff --git a/Subsurface/Source/Networking/NetStats.cs b/Subsurface/Source/Networking/NetStats.cs index 542543e72..9d8025344 100644 --- a/Subsurface/Source/Networking/NetStats.cs +++ b/Subsurface/Source/Networking/NetStats.cs @@ -73,12 +73,12 @@ namespace Barotrauma.Networking spriteBatch.DrawString(GUI.SmallFont, "Peak received: "+graphs[(int)NetStatType.ReceivedBytes].LargestValue()+" bytes/s " + - "Avg received: " + graphs[(int)NetStatType.ReceivedBytes].LargestValue()/Graph.ArraySize + " bytes/s", + "Avg received: " + graphs[(int)NetStatType.ReceivedBytes].Average() + " bytes/s", new Vector2(rect.X + 10, rect.Y+10), Color.Cyan); spriteBatch.DrawString(GUI.SmallFont, "Peak sent: " + graphs[(int)NetStatType.SentBytes].LargestValue() + " bytes/s " + - "Avg sent: " + graphs[(int)NetStatType.SentBytes].LargestValue()/Graph.ArraySize + " bytes/s", + "Avg sent: " + graphs[(int)NetStatType.SentBytes].Average() + " bytes/s", new Vector2(rect.X + 10, rect.Y + 30), Color.Orange); spriteBatch.DrawString(GUI.SmallFont, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", @@ -107,6 +107,11 @@ namespace Barotrauma.Networking return maxValue; } + public float Average() + { + return values.Average(); + } + public void Update(float newValue) { for (int i = values.Length-1; i > 0; i--) diff --git a/Subsurface/Source/Networking/NetworkEvent.cs b/Subsurface/Source/Networking/NetworkEvent.cs index b2b3c2f94..8195c75b6 100644 --- a/Subsurface/Source/Networking/NetworkEvent.cs +++ b/Subsurface/Source/Networking/NetworkEvent.cs @@ -7,24 +7,58 @@ namespace Barotrauma.Networking enum NetworkEventType { EntityUpdate = 0, - KillCharacter = 1, - UpdateComponent = 2, - DropItem = 3, - InventoryUpdate = 4, - PickItem = 5, - UpdateProperty = 6, - WallDamage = 7, + ImportantEntityUpdate = 1, + + KillCharacter = 2, + SelectCharacter = 3, + + ComponentUpdate = 4, + ImportantComponentUpdate = 5, + + PickItem = 6, + DropItem = 7, + InventoryUpdate = 8, + + + UpdateProperty = 9, + WallDamage = 10, + - SelectCharacter = 8, - EntityUpdateLarge = 9 } class NetworkEvent { public static List events = new List(); - private static bool[] isImportant = { false, true, false, true, true, true, true, true, true, false }; - private static bool[] overridePrevious = { true, false, true, false, false, false, true, true, true, true }; + private static bool[] isImportant; + private static bool[] overridePrevious; + + static NetworkEvent() + { + isImportant = new bool[11]; + isImportant[(int)NetworkEventType.ImportantEntityUpdate] = true; + isImportant[(int)NetworkEventType.ImportantComponentUpdate] = true; + isImportant[(int)NetworkEventType.KillCharacter] = true; + isImportant[(int)NetworkEventType.SelectCharacter] = true; + + isImportant[(int)NetworkEventType.ImportantComponentUpdate] = true; + isImportant[(int)NetworkEventType.PickItem] = true; + isImportant[(int)NetworkEventType.DropItem] = true; + isImportant[(int)NetworkEventType.InventoryUpdate] = true; + + isImportant[(int)NetworkEventType.UpdateProperty] = true; + isImportant[(int)NetworkEventType.WallDamage] = true; + + overridePrevious = new bool[11]; + for (int i = 0; i < 11; i++ ) + { + overridePrevious[i] = true; + } + overridePrevious[(int)NetworkEventType.KillCharacter] = false; + + overridePrevious[(int)NetworkEventType.PickItem] = false; + overridePrevious[(int)NetworkEventType.DropItem] = false; + } private ushort id; @@ -103,6 +137,10 @@ namespace Barotrauma.Networking catch { +#if DEBUG + DebugConsole.ThrowError("Failed to write network message for entity "+e.ToString()); +#endif + return false; } @@ -121,7 +159,9 @@ namespace Barotrauma.Networking } catch { +#if DEBUG DebugConsole.ThrowError("Received invalid network message"); +#endif return false; } diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index 729d14b98..eb5eff609 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Networking private bool crewFrameOpen; private GUIButton crewButton; - private GUIFrame crewFrame; + protected GUIFrame crewFrame; protected bool gameStarted; @@ -117,20 +117,21 @@ namespace Barotrauma.Networking protected void CreateCrewFrame(List crew) { - int width = 500, height = 400; + int width = 600, height = 400; crewFrame = new GUIFrame(new Rectangle(GameMain.GraphicsWidth / 2 - width / 2, GameMain.GraphicsHeight / 2 - height / 2, width, height), GUI.Style); crewFrame.Padding = new Vector4(10.0f, 10.0f, 10.0f, 10.0f); - GUIListBox crewList = new GUIListBox(new Rectangle(0, 0, 300, 300), Color.White * 0.7f, GUI.Style, crewFrame); + GUIListBox crewList = new GUIListBox(new Rectangle(0, 0, 280, 300), Color.White * 0.7f, GUI.Style, crewFrame); crewList.Padding = new Vector4(10.0f, 10.0f, 10.0f, 10.0f); - crewList.OnSelected = SelectCharacter; + crewList.OnSelected = SelectCrewCharacter; foreach (Character character in crew) { GUIFrame frame = new GUIFrame(new Rectangle(0, 0, 0, 40), Color.Transparent, null, crewList); frame.UserData = character; frame.Padding = new Vector4(5.0f, 5.0f, 5.0f, 5.0f); + frame.Color = (myCharacter == character) ? Color.Gold * 0.2f : Color.Transparent; frame.HoverColor = Color.LightGray * 0.5f; frame.SelectedColor = Color.Gold * 0.5f; @@ -142,14 +143,14 @@ namespace Barotrauma.Networking null, frame); textBlock.Padding = new Vector4(5.0f, 0.0f, 5.0f, 0.0f); - new GUIImage(new Rectangle(-10, -10, 0, 0), character.AnimController.Limbs[0].sprite, Alignment.Left, frame); + new GUIImage(new Rectangle(-10, 0, 0, 0), character.AnimController.Limbs[0].sprite, Alignment.Left, frame); } var closeButton = new GUIButton(new Rectangle(0,0, 80, 20), "Close", Alignment.BottomCenter, GUI.Style, crewFrame); closeButton.OnClicked = ToggleCrewFrame; } - private bool SelectCharacter(GUIComponent component, object obj) + protected virtual bool SelectCrewCharacter(GUIComponent component, object obj) { Character character = obj as Character; if (obj == null) return false; @@ -174,6 +175,17 @@ namespace Barotrauma.Networking return true; } + //protected void UpdateCrewFrame(List connectedClients) + //{ + // List characterList = new List(); + // foreach (Client c in connectedClients) + // { + // if (c.character != null && c.inGame) characterList.Add(c.character); + // } + + // CreateCrewFrame(characterList); + //} + public bool EnterChatMessage(GUITextBox textBox, string message) { if (string.IsNullOrWhiteSpace(message)) return false; diff --git a/Subsurface/Source/Screens/MainMenuScreen.cs b/Subsurface/Source/Screens/MainMenuScreen.cs index 44201ed49..3a9bed0be 100644 --- a/Subsurface/Source/Screens/MainMenuScreen.cs +++ b/Subsurface/Source/Screens/MainMenuScreen.cs @@ -243,6 +243,8 @@ namespace Barotrauma return false; } + GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.NetworkMember = new GameServer(name, port, isPublicBox.Selected, passwordBox.Text, useUpnpBox.Selected, int.Parse(maxPlayersBox.Text)); GameMain.NetLobbyScreen.IsServer = true; diff --git a/Subsurface/Source/Screens/NetLobbyScreen.cs b/Subsurface/Source/Screens/NetLobbyScreen.cs index 4e1cd1ae0..5afaf1bbc 100644 --- a/Subsurface/Source/Screens/NetLobbyScreen.cs +++ b/Subsurface/Source/Screens/NetLobbyScreen.cs @@ -467,11 +467,16 @@ namespace Barotrauma GUI.Style, Alignment.TopLeft, Alignment.TopLeft, playerFrameInner, false, GUI.LargeFont); - var kickButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Kick", Alignment.BottomLeft, GUI.Style, playerFrameInner); + var kickButton = new GUIButton(new Rectangle(0, -30, 100, 20), "Kick", Alignment.BottomLeft, GUI.Style, playerFrameInner); kickButton.UserData = obj; kickButton.OnClicked += KickPlayer; kickButton.OnClicked += ClosePlayerFrame; + var banButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Ban", Alignment.BottomLeft, GUI.Style, playerFrameInner); + banButton.UserData = obj; + banButton.OnClicked += BanPlayer; + banButton.OnClicked += ClosePlayerFrame; + var closeButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Close", Alignment.BottomRight, GUI.Style, playerFrameInner); closeButton.OnClicked = ClosePlayerFrame; @@ -485,11 +490,20 @@ namespace Barotrauma return true; } - private bool KickPlayer(GUIButton button, object userData) + public bool KickPlayer(GUIButton button, object userData) { if (GameMain.Server == null || userData == null) return false; GameMain.Server.KickPlayer(userData.ToString()); + + return false; + } + + public bool BanPlayer(GUIButton button, object userData) + { + if (GameMain.Server == null || userData == null) return false; + + GameMain.Server.KickPlayer(userData.ToString(), true); return false; } diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index 2f3da2e21b7bee79c132b66784073c96f88875d4..4cd6ea86e451d83a42c529dae5b73a7076ced2b6 100644 GIT binary patch delta 15928 zcmdUW3tW`dw)pJt+uzLh0!F|Q32~f8L_{1B%_okCrsM<3$_PzVEE5qC@e%20FEce@ zY*wdJ9U>Zy%<#pwGMmU_mS$*X9uEyLaoFwfEZZ zwbr-y+G}rI2NPZAdXzXu8?Fbl!O&MQ7%Z5+dhOaZP9_K@1P8(fOniwj6w9O{3_?go za3Z{d^kBph?m4}owB-9TAy-OwE^v%;&k-IK4wyFoEU1KRa}ydC6p!UP9x=@=c@h*8~52pJd@F_+kju+gCa&j=wYCQ(h(ftRRwH$z%4}cGr4}e#2 zUsfKZ{>X!BY!Hv|&;`4Ub$_jfnY&{w0fG0$1Na`JvE=IJZ&i)BPpZ17h@6pm<_E}R zxKKo*=zboHm`@y%zY>g|PsaMYfb#j69cX1fak^cdZJTYu4W#`@9Ml^)pm|M$T7&6@ z2yeODPwCjX6k|5bxq$c>;y)p_Al{8|C&t?$mfRDkbcyPYu>%OtAvh4K5qNhzxXY&` zrSQm;SY;q`pe@3DjQ<%6l_6e(cpu`25vnj=j(92JR}qg!`~~8J2DfEufBkk$K91mT zH*0DX1Z!!KZ;){~O&|h%xn;P9_I+EJsI5r|a zi&+J5MG(7ewujc6{td>Kbws{KHMDERSFWr7I$w3p*NB_(mAB`9VM^bWWjOACpQq|| zo?3}Jb82)mN3;HUjt1nBfUKO1umFekG(sH4havoq)tFb&hnAUBI1BLU^1N3s1m24-VOh&=Da8Aq44}i1#B@ zVg70ZB&P|Hl7{KO!^AY9hy78E9!H46Y~IT|2r-zx2jK+9-$sDyG{H(fh5czlH)#T9 zTmW*1aLbZe7)?iLi@=x6E(nPjZ9oXa_yUA1j5`rD9EyUFiZm1AV>tCjgr_jQ4&eZn zc>wXxhza7`5OyKR2)`n{h4gU5_afYfaL!=3#c&)On2HICnJVH>5Yjj{K+PRObhl!R z8?dvnh!-I&#^e!%yD&btwuDBN^ z$8NS&H4tw)5y%l(I!_!4lTwwkt@Xri`gfJ#5b}g8Lowq2OUkhIpHPOl-zvjeoaD`w z!F{#FzSgD^%51sBM&yYo?FU;)yl@s-Sz{*FV6eJ`3Brp8So9=`W0fu;iVLZLNb=!XwR+g1w8*%Af&*gwn99cJ)D%9JQIZQu1}-T&Z8p7BJPc?y?}T*!cm0Bah`v} zy!IIX4hBsS+6A4)Xgop#l3#@E2}0PCKVzvkk@ORS7oiT**AVj+dobd|2;tcB2Z%c% zE=BA^cpsqwA&i&DkcaU{Fx~<2s|Z^#{#V535eH$tY>WqE{1t?su=_2T{*V|Z8Dfxd z0SWD}P!JNAV7eRPy$HP!Uc>ZPh`+}&`!W6w#$QKx1|baVe1>>D(!axe|3-@GXoMpO zIWX>Coa0H%YQ*G0g!2e|Zr>nYkMIS;LkRTtNKU<>XI_ zp$Pk0gwSGTT263f3dfb{DYDYLnnZaf5PuD3-z#(^3Ym|^7q+TW_=&KROabziFv6YR zHHQ5p5JxPQbAF+iqTZE87q(7ti_b^4^Z~DoDBIy zHA4PaC4yzygi6hGr)URzh>-3sy!@$1#tAU?HL)`eR&f_z*}!ZLp`pEJl_0@zRZW6^ zn}m^2d%Kv-e47L(@pK?|cg(R!mK;r15PA!0%?);J$N9_dizQu|ZH&;M1e7L1yh_~G zM{v%WLVXCyh4-t76RMXgQSjm~qB$%#_REeFOP}p<|M*X4^gFd}HE(g_8l?w&^Pn(} zctmcBk-M|A56fz8?H%A56f``P3e9oqDCIv20XO0|E@WrgUT74hf&5 z+VM5wf`|q|gJAYn;a7D=ATkIxEgVL67VOYZpdXUkfQYoxEI!iD5VU>YUwi+MvQFRd z=!dX$h1L#Mz9A-gN1*H$!4L~c1g8~MFz;JAoIQM5cwB@zqr_3>#y9)5uL)V(^Jqry zPw(z;-^c$|v-~OI5E3%=>WN`;%&~39lO8)#bfy%1Q$=S$hXk=+`WF_6G6zmZ<&)W@ z5zCpOIBy=Z4O0y{Xi4HqX~=_4ZL~P>RgodE^l5DgxT3^q;M#!_joT|lvXWpiU-XQp zsK5!dlF-;zYsX0B-XG!OkK*Exl0^?}JK3E^2JMp0obVRFxB{Uq`{P+Lhj`ixSbH+X zPr1!NwL5fQsjY#6nUckxf8!_pN!Ss_64ypM1l@lKc7@ zqdM>izz+c^tiP-xUC7=13mgotzYv$-K8O?>Br+Mu36cioM{KT} z)E8u7x4v3@~ZAHZV-b80S3?1%3;E6TGop>rRSb$oFCfZ2Mla!~CbTr@W~m z^(>|({xf_}cy=-E=8q1CKVy_~?S^xUsWlLbSVE)Za%`CEISgDv9}CcTE}==v^)I0S z$Sf3a<1rB?v2}f_jr0hqTtIhd<>oHQF!;PB(D+tAdnyFoFB`c zl}5oRpKub1*oMiz`EpU+nPtidVZuUMcE)@WR;zuyhIbcILQfCVqv@LWLb9BX2I^Tv8ER z5f>u<-(i(;E9d`;RmL^pW>(o*S2170hbHJA= z+t`{CF;^rH!O}bQG-x~`S=mgh@GOCzF10;t?-Bo|K-uGp10096bZ~VPo#yMS^*tlj z?qNoK7}Ooo9Ojc_ZwnoLdFC_CRPz_w8E=IcK^E{8aRlUlBL|yv=5_jf>x9j@i!N+D zJmA6~-wK$FU)?K8zi*)AfR+TV1Gv34|By_+kdAOy_X&mjH2Uh2Jt*!&2%J}|5F3$S zMTlQRc)p3Ah&ThG7v}v5u^BN}y(f^1zo5Bt8F4K_FN8Ljw+V3+;%^c2a#yg9is@5W z_7ujiBFx6TF~ksUIEoP)W^P3yA4JPwac_JgEVVo4CLqlCO^NTYrUmJ|#%)dbC!~#P zDwo(q>x^mlIo+ys#{O?dVwQVO(4Fr5pb~EZ>g=XRgwT+Oss~@0(2&1*(bJD=UTb%F zxxd(!^{E#=Cf-_+k}Ut8#GK`#k9ZLB$I(E)|S$SN3G zpxq7)Lx>H|bRnuceUJ{WLt=(`;Y8tz^UyaRR#(NO*k7FZDYvDfyW zhJ_z>SoYblMGICKes=HggBx!qZoJ%f;*}jkm0&P;k#shZQoDG=+)<|1_6~}!-1^Z| zUYQEG{`psk9{Isr?-6^Ekf#fF4YXgAI*lJ2I%xKVVK8p6V1YM@I1cuQ$lY0Yoqk5z z=b*I{F!H)?dSZNRajz#{-`XMXwm+PQ{ST0~EFn>Rj)VmLaltOfno+YCo;cfi(A*E8 zbPyVAr+SGN_|Yx3Wo^y$2f}D7-A+umCc^Rs_R4@gS3iu}z2Q{wM9@NEq5+B`=vCo9 z#O>)Axf>c^15nMIKSGV~e%!{uz^>gviQtW-bmP)|()mkV06O3Y`xWA@DE_YzA4Rwu z;~yb>gYgsa$SYzySRY31c+^ENE7EEZ??Ct*;S#2QMM%Q9`%Jq;sE8yn^uE^1z$7tU z&P3AmR^)S&#PROk;qg#-zqpDK8@;C0B+{J(c1F;Fp2-v~dIv=p#uG~f^Tv{VQd;7I z(kG>^tZNeef_T=Gp$Zv@)3xEunx_ke>xoNv$^bvFCyS-AII=ug_y)N{=I$43%aTFM zG1bHv1MUM{DO|l26~=VI<>bD;^i|?{h!&D@FfW5HA?a`)k;IRT1Kk&eTj7N}=>*g3 z*#D?X!hRh<7m84B(ka0sb#zy#ct^}(rd0YCG4<#(z&9zu>=zzG;eAgi-x`{StX1OK zf?IzW*tNOhcw(OPQ)1qF%XdS=Um8<;$#T~KAitnmESx7k3Hz6+z08;1ms1+@g`?#q z=WcOC4?N>vyV%5O;w$8Ta@DGNh@K<*HJtO2S5V#bZC$nP%9nob)DzozQUEqo4N;c4B6 z@F_wq!gb5vzl=fK`J{)^yrTVw*MfL^#cav6lfb>Itk6j}5M~=ry}uEXGW#V_gVTq^KtXo2VB ziM(bCUL=z{pwm=(iz5RI+=;;5zIP#@>&I{p0++?TO_;~|9TdkhJkET$udt0{D%dw}M81?=6Zxj_ov(t1SP?r8VJxyR!6}2$S4tgrv zTW%C!+3VB-JDenfB~;M^#2X_CaP%u0#iG{J)g?fEhEahEv$wq9TCkfQvEj{Gf zX$)tJYmLi95Z^Z8&3F%O?V-vnvcz;kg2a#M+fdRDYag$pQN$Z%G_Os0*eG}pP+@J#rzU~bdTDZ4 zYay*SvXuR%6%;u~+3NAe0tUPFakT*4FMqqOgsIL-a)B2$uD> zsWN13?Z-$wT5QZ@nG1|w!fN-?WRW2sYAKveFye}}*VHI^1qn-aGERp&-FVv514msK zWPILhlp^5lI^%NB6`BCn5Mvcge$80!eaWakE9eg9*knW}b-X0QfjeX;a~?7ILdg?xiMuf@8ZQ%k? z!Epf@bv4hp2^N`*#c;rC%;S>}hkfg%C>H&m=_o2vxr_`t%o+4H3hAiH>@7A?SUuZl z1!$~ zNu`VmQGHs&8_zm99DKLPl*Me9kKku?iB9HCmxr|ON+58cJvx!Y_sZWBclG0DX0DUZ zis0)kM>FdoxsgEmPjWCgCTbRDKO&>Y&e2sK&&D5>*NbbX%rOb>jJ_5yua_MVJXbZc zQ77arlG_=Fk~}Tf5r}ju3D7tWvnOv;!lAA=cJ{g-s{E`H2hWYe>F$)3kJ*&DrY8czht(TR z1#onv(ueI-l#fl`G?}hl-9{0-hwuYS>@N3*>Yj20YYbC1bO4)0S^$lo(0I=3EaJ`> zRZx?ttd^jnz+`JJ*?O3~RT>CeD)FMG{6$pKQ@1G=wd}9KEI3s;OTc-LG9I28AcnIC zM<_8899V36f-UYNMH5u03Rp4E6vd|eY&xQf1%{dMb(YkYH@My_$id!FS%4`U)l7DB zwBjY+a2fB(y2=)){!CVxHJZy~QztEj)s0nlpfhc# zM%e0cN`VX|`{X!i{6>le^Df!QCS)mnL^v>0>d*RT$h+FXJR1L;qu$? zlu}K+2a&dEp%TTao>q>Fu<1#88kE1PSlFV&GD`KRL&?Y29%yFRN|apOXZj1V5IsjZ z?EO+EaCR4}Rz<1uJ>KS@SJS|}U$(Hx4RVDD&MC4Ejn6AdEaiI{`8MScUY;CWu09WE zZdJ@Ica5?J#pYBgl&?|~;qBLzNS6MRl57IoOr;N$EK=+&^Nj38jvtm|q2^U34H`Bo zsH*Fgd>qGHav$(rk`vj3XXWS6Q(}~(!BI^@nd1`w21@e;3#)uX$r7L>OCAqrK2yrT z_nO+5MQ>K}kqtZK2~d3>p0FE=6&qW9SuQ~3?x8IT9FqLs)e5Q<5kLz8TiVK7s=HH- zg~r8_4(0c&p=|iu$_P@Eq6h*jc}K}NgYRWI34Ajp^Z>0PGMiMVR1mQKDv#mMl2E)q zp!gm-qTv4L`@4ddXHLm>t_sj7tzDS)C;@-4lf+vGtlX(0sKdu&_Jw8B=fJy0i z%MkN|`j7^XkNf%rJ7r%pw+P7ZWjg`h}5zPBn6-}sR zTku`NnXg4NjJc1`w?V1y46YJ+8BBgdR>7RCMzM89bpnBsd(kqi_>xQR=rK74#@69` znRHpL4uO&hN?%Vpc9$QimJrDQGa5yc@6n8`>MQj)_Hh(t7#*b^4FT)@s>;;6)t_+? z4@)r+H3>(u_iHst1)n71mVQ?CiQWsU4t3wE<01ZaEsEtEwTDE=$UvE(b1|Oz(o`pr zb+q01*%xG0>szC0OG!`=K2i%%HN$ZCoZ&c#_vNVIF6NukYjn+L@@Ar@{&*GLJm$x= z?})o>bPQCFQX{}I9dDec6{;`b+Q9{4Zl~GdLR)PV31O}$v)-df3*bt({2Nj|#dA1?fe?gbRZ#+}T7>>rEtDB=%1p+sCP|y1q{J9VWK^59q zQq8aEQqb4$uT}^p7ljUHeoErg9uc^obU?yptqbf~sYOHW6vYndQ`Ktl9VT}Z2h_bP zSz(s0Df|hZKR{ivW`R$OH5)h&XyYY*edy%v$HMyuw83B=gtxGD(KH^eex{{(SEI48 zX@L|8d2y(TzX%+10sa%WGOvhYR=Jz@Q8+B_)zQ0qxvxzSeeyuukc&AHh{ zIXvd?3;wWC3nu?83#npA{U>Ipt0~!eqj0VXQ|I=E{U2)GOictJ`?dBKdy{V9+YMhb z#zPQN|JgCLYdiG1d`DiFFZMHl_hSti>6d472?*Y!*(@>FmC<1H^}{%KUZnnqOb*g+ zN1l&Ho@2A!{LRYODS`1f4<94^@jZvssvC@k6Kl0dc;Q3s(KcRm5(Gko&xi3I;MlE| z`1J&y|5%HG$@?@J_iADx#ccOycg$j!E>FH;GFi*|Rx=#PS5O}+ByzLgBc=~_OC$9L{2EHml zT^D@-Ji1*AwQ@&gAjJFlLq4A7W8nS@9)R?dyMQ$vP1c`Rsw#nLpWq%@_nDRqWrwuW zaQYA~HfYOn6F#c>{nKbVTkyuM zdb6Ac&SP30tnaUd!l9#D71(msWS0D#S}VfpBEI!)P|=(j-%E+-S0z!f`3) zSoV#!Se%PP^exqT^Q}Y8kgIigsc+IBj|?>tNMq3GjJ0 zJr{fqJ(BIP=@$f;6O88OhKp(v8*y62BXap-HJ9c7tm26!*s6Dc&pPXWh4rzz4F>hl z6WN9s{UO3IokqMA<+Lvz*=df`@r7rseh28?^p7ASRv!s@cZi`-Jr`dmmBi~38y>G$ z3b44Ru7S&;$H49c{Z1TTEIi&(x5Mln`f>jt!5fD?rFF-jx=zTFGYPuRj^JpCR^T^= za{2VxwT&A2hi}8ee*5MCACJMc$B@&)<65YYU;x+Cn#(I_f;R$ZF=e4v#EOjCiZ(To z+L8MqrHdSc2BD7xhu+*G^b~7PvPX37LxmWBZ_JV3S!4K?Y8v_dgDJ)zOc4tTbsh?J zItsPrBe!Pb@cN(`4%$h;yji=~|DudrX}HGVd$Pb)(O}PChy}*&(c;iwyYzrI^v3fD zf9$t!B+xpuXy1eezEst4vmGy{r7dA;Vf{2k(U_S6pQyAKH&NC=^@mz0X|c!sqm8G9 z{gz^CvhjFR-O0ulTQ9c&+fJQ5D}Qz#+IMK}%y=L#J2z|oi~@N1J*^}BU8O&N-9%A1jAq;#0ytyZa}o1}4aV19N!4xjFoE1B~+*%m1=j6^8Fv1^c{$>Dg0r zvhJKcuYe=JeK$AjCN@?Z{`X6PVD^27aT|QNP3-rdt74lFY>l5yhH>r0N%iNG>ZZ4( z2BuYXy#qtIkKfAh(9&H02cLQOP;*mzqw&MfDZp<0hI(^1ezIRF%l+*Je)#^&&(*2_ z>E~h&&Io)!O_E9SOQuagGA*+FGjZQ4gbS(v>T02y?wL`LgCifD14FEOgkRU90zC}< zopSTC{?0n`zw`)%&OR^3IqO;&iLh~k+!360xY>ujrTx9861NW`_@C2ed?3Wv4A;@r z@g2Yg-2dcvgxj${f@QDMI->2A+~UlbYC@d5&m9 z7`aCuEP(Hb=JUR-p~JBk`p@$Y3!!+?=YKWm`wHF855J>54s}PgYB=+ymR>VaccJ4n zQ9&CQ|AfHn!+Ng!(Yp0DXfDrId}`P@NVa(oYv!6-9IiW~9t)PUGTjk~(}kQt$o$02pI)mH+?% delta 14905 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