diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs index 8a9f1ebc4..a2c3e861b 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs @@ -13,6 +13,8 @@ namespace Barotrauma private static Sprite noiseOverlay, damageOverlay; private static GUIButton cprButton; + + private static GUIButton grabHoldButton; private static GUIButton suicideButton; @@ -41,6 +43,8 @@ namespace Barotrauma if (cprButton != null && cprButton.Visible) cprButton.AddToGUIUpdateList(); + if (grabHoldButton != null && cprButton.Visible) grabHoldButton.AddToGUIUpdateList(); + if (suicideButton != null && suicideButton.Visible) suicideButton.AddToGUIUpdateList(); if (!character.IsUnconscious && character.Stun <= 0.0f) @@ -89,6 +93,8 @@ namespace Barotrauma if (cprButton != null && cprButton.Visible) cprButton.Update(deltaTime); + if (grabHoldButton != null && grabHoldButton.Visible) grabHoldButton.Update(deltaTime); + if (suicideButton != null && suicideButton.Visible) suicideButton.Update(deltaTime); if (damageOverlayTimer > 0.0f) damageOverlayTimer -= deltaTime; @@ -195,9 +201,37 @@ namespace Barotrauma }; } + if (grabHoldButton == null) + { + grabHoldButton = new GUIButton( + new Rectangle(character.SelectedCharacter.Inventory.SlotPositions[0].ToPoint() + new Point(320, -60), new Point(130, 20)), + "Grabbing: " + (character.AnimController.GrabLimb == LimbType.Torso ? "Torso" : "Hands"), ""); + + grabHoldButton.OnClicked = (button, userData) => + { + if (Character.Controlled == null || Character.Controlled.SelectedCharacter == null) return false; + + Character.Controlled.AnimController.GrabLimb = Character.Controlled.AnimController.GrabLimb == LimbType.None ? LimbType.Torso : LimbType.None; + + foreach (Limb limb in Character.Controlled.SelectedCharacter.AnimController.Limbs) + { + limb.pullJoint.Enabled = false; + } + + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Control }); + } + + grabHoldButton.Text = "Grabbing: " + (Character.Controlled.AnimController.GrabLimb == LimbType.Torso ? "Torso" : "Hands"); + return true; + }; + } + //cprButton.Visible = character.GetSkillLevel("Medical") > 20.0f; if (cprButton.Visible) cprButton.Draw(spriteBatch); + if (grabHoldButton.Visible) grabHoldButton.Draw(spriteBatch); } if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index 82418a2a4..51d728219 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -17,15 +17,19 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 2, 0); + msg.WriteRangedInteger(0, 3, 0); inventory.ClientWrite(msg, extraData); break; case NetEntityEvent.Type.Repair: - msg.WriteRangedInteger(0, 2, 1); + msg.WriteRangedInteger(0, 3, 1); msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(0, 2, 2); + msg.WriteRangedInteger(0, 3, 2); + break; + case NetEntityEvent.Type.Control: + msg.WriteRangedInteger(0, 3, 3); + msg.Write((UInt16)AnimController.GrabLimb); break; } } @@ -91,6 +95,7 @@ namespace Barotrauma bool crouching = msg.ReadBoolean(); keys[(int)InputType.Crouch].Held = crouching; keys[(int)InputType.Crouch].SetState(false, crouching); + AnimController.GrabLimb = (LimbType)msg.ReadUInt16(); } bool hasAttackLimb = msg.ReadBoolean(); @@ -367,6 +372,9 @@ namespace Barotrauma SetStun(0.0f, true, true); } + bool ragdolled = msg.ReadBoolean(); + IsRagdolled = ragdolled; + bool huskInfected = msg.ReadBoolean(); if (huskInfected) { diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index cee0e43da..539531cd2 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -15,6 +15,8 @@ namespace Barotrauma private static Queue queuedMessages = new Queue(); + private static GUITextBlock activeQuestionText; + public static bool IsOpen { get @@ -69,6 +71,13 @@ namespace Barotrauma } } + if (activeQuestionText != null && + (listBox.children.Count == 0 || listBox.children[listBox.children.Count - 1] != activeQuestionText)) + { + listBox.children.Remove(activeQuestionText); + listBox.children.Add(activeQuestionText); + } + if (PlayerInput.KeyHit(Keys.F3)) { isOpen = !isOpen; @@ -105,7 +114,7 @@ namespace Barotrauma if (PlayerInput.KeyHit(Keys.Enter)) { - ExecuteCommand(textBox.Text, game); + ExecuteCommand(textBox.Text); textBox.Text = ""; } } @@ -134,7 +143,7 @@ namespace Barotrauma case "entitylist": return true; default: - return false; + return client.HasConsoleCommandPermission(command); } } @@ -177,13 +186,6 @@ namespace Barotrauma } selectedIndex = Messages.Count; - - if (activeQuestionText != null) - { - //make sure the active question stays at the bottom of the list - listBox.children.Remove(activeQuestionText); - listBox.children.Add(activeQuestionText); - } } private static void InitProjectSpecific() diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs index ce95757ca..8516d994c 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs @@ -115,10 +115,11 @@ namespace Barotrauma listBox.AddChild(child); } - public void AddItem(string text, object userData = null) + public void AddItem(string text, object userData = null, string toolTip = "") { GUITextBlock textBlock = new GUITextBlock(new Rectangle(0,0,0,20), text, "ListBoxElement", Alignment.TopLeft, Alignment.CenterLeft, listBox); textBlock.UserData = userData; + textBlock.ToolTip = toolTip; } public override void ClearChildren() diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs index b1cd60307..dc45399d9 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs @@ -171,12 +171,12 @@ namespace Barotrauma { for (int i = 0; i < children.Count; i++) { - if (!children[i].UserData.Equals(userData)) continue; - - Select(i, force); - - //if (OnSelected != null) OnSelected(Selected, Selected.UserData); - if (!SelectMultiple) return; + if ((children[i].UserData != null && children[i].UserData.Equals(userData)) || + (children[i].UserData == null && userData == null)) + { + Select(i, force); + if (!SelectMultiple) return; + } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs index 0c2477986..8e9de9c14 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs @@ -7,12 +7,8 @@ namespace Barotrauma { public static List MessageBoxes = new List(); - const int DefaultWidth=400, DefaultHeight=250; - - //public delegate bool OnClickedHandler(GUIButton button, object obj); - //public OnClickedHandler OnClicked; - - //GUIFrame frame; + public const int DefaultWidth = 400, DefaultHeight = 250; + public GUIButton[] Buttons; public static GUIComponent VisibleBox diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs index 27dea057a..e6e0f5734 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs @@ -50,8 +50,8 @@ namespace Barotrauma { base.Rect = value; - box.Rect = new Rectangle(value.X,value.Y,box.Rect.Width,box.Rect.Height); - text.Rect = new Rectangle(box.Rect.Right, box.Rect.Y + 2, 20, box.Rect.Height); + if (box != null) box.Rect = new Rectangle(value.X,value.Y,box.Rect.Width,box.Rect.Height); + if (text != null) text.Rect = new Rectangle(box.Rect.Right, box.Rect.Y + 2, 20, box.Rect.Height); } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs index 668f7ac08..75f22a273 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs @@ -1,9 +1,5 @@ using Lidgren.Network; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Barotrauma.Networking { @@ -40,6 +36,10 @@ namespace Barotrauma.Networking { new GUIMessageBox("", txt); } + else if (type == ChatMessageType.Console) + { + DebugConsole.NewMessage(txt, MessageColor[(int)ChatMessageType.Console]); + } else { GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index 7dc08f89f..8052b89d4 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -19,6 +19,7 @@ namespace Barotrauma.Networking private GUITickBox endVoteTickBox; private ClientPermissions permissions = ClientPermissions.None; + private List permittedConsoleCommands = new List(); private bool connected; @@ -598,16 +599,28 @@ namespace Barotrauma.Networking private void ReadPermissions(NetIncomingMessage inc) { + List permittedConsoleCommands = new List(); ClientPermissions newPermissions = (ClientPermissions)inc.ReadByte(); - if (newPermissions != permissions) + if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands)) { - SetPermissions(newPermissions); - } + UInt16 consoleCommandCount = inc.ReadUInt16(); + for (int i = 0; i < consoleCommandCount; i++) + { + permittedConsoleCommands.Add(inc.ReadString()); + } + } + + SetPermissions(newPermissions, permittedConsoleCommands); } - private void SetPermissions(ClientPermissions newPermissions) + private void SetPermissions(ClientPermissions newPermissions, List permittedConsoleCommands) { - if (newPermissions == permissions) return; + if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || + permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) + { + if (newPermissions == permissions) return; + } + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "permissions"); string msg = ""; @@ -626,8 +639,22 @@ namespace Barotrauma.Networking msg += " - " + attributes[0].Description + "\n"; } } + permissions = newPermissions; - new GUIMessageBox("Permissions changed", msg).UserData = "permissions"; + this.permittedConsoleCommands = new List(permittedConsoleCommands); + GUIMessageBox msgBox = new GUIMessageBox("Permissions changed", msg, GUIMessageBox.DefaultWidth, 0); + msgBox.UserData = "permissions"; + + if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands)) + { + int listBoxWidth = (int)(msgBox.InnerFrame.Rect.Width - msgBox.InnerFrame.Padding.X - msgBox.InnerFrame.Padding.Z) / 2 - 30; + new GUITextBlock(new Rectangle(0, 0, listBoxWidth, 15), "Permitted console commands:", "", Alignment.TopRight, Alignment.TopLeft, msgBox.InnerFrame, true, GUI.SmallFont); + var commandList = new GUIListBox(new Rectangle(0, 20, listBoxWidth, 0), "", Alignment.BottomRight, msgBox.InnerFrame); + foreach (string permittedCommand in permittedConsoleCommands) + { + new GUITextBlock(new Rectangle(0, 0, 0, 15), permittedCommand, "", commandList, GUI.SmallFont).CanBeFocused = false; + } + } GameMain.NetLobbyScreen.SubList.Enabled = Voting.AllowSubVoting || HasPermission(ClientPermissions.SelectSub); GameMain.NetLobbyScreen.ModeList.Enabled = Voting.AllowModeVoting || HasPermission(ClientPermissions.SelectMode); @@ -805,7 +832,7 @@ namespace Barotrauma.Networking gameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); - SetPermissions((ClientPermissions)inc.ReadByte()); + ReadPermissions(inc); if (gameStarted) { @@ -1169,6 +1196,14 @@ namespace Barotrauma.Networking return permissions.HasFlag(permission); } + public bool HasConsoleCommandPermission(string command) + { + if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) return false; + + command = command.ToLowerInvariant(); + return permittedConsoleCommands.Any(c => c.ToLowerInvariant() == command); + } + public override void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { base.Draw(spriteBatch); @@ -1359,6 +1394,25 @@ namespace Barotrauma.Networking client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); } + public void SendConsoleCommand(string command) + { + if (string.IsNullOrWhiteSpace(command)) + { + DebugConsole.ThrowError("Cannot send an empty console command to the server!\n" + Environment.StackTrace); + return; + } + + NetOutgoingMessage msg = client.CreateMessage(); + msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); + msg.Write((byte)ClientPermissions.ConsoleCommands); + msg.Write(command); + Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + msg.Write(cursorWorldPos.X); + msg.Write(cursorWorldPos.Y); + + client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + } + /// /// Tell the server to select a submarine (permission required) /// diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameServerSettings.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameServerSettings.cs index 7c694d39a..62410b7bf 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameServerSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameServerSettings.cs @@ -320,7 +320,7 @@ namespace Barotrauma.Networking return true; }; - y += 40; + y += 20; var voteKickBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Allow vote kicking", Alignment.Left, settingsTabs[1]); voteKickBox.Selected = Voting.AllowVoteKick; @@ -357,7 +357,7 @@ namespace Barotrauma.Networking return true; }; - y += 40; + y += 20; var randomizeLevelBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Randomize level seed between rounds", Alignment.Left, settingsTabs[1]); randomizeLevelBox.Selected = RandomizeSeed; @@ -367,7 +367,7 @@ namespace Barotrauma.Networking return true; }; - y += 40; + y += 20; var saveLogsBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Save server logs", Alignment.Left, settingsTabs[1]); saveLogsBox.Selected = SaveServerLogs; @@ -378,6 +378,83 @@ namespace Barotrauma.Networking return true; }; + y += 20; + + var ragdollButtonBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Allow ragdoll button", Alignment.Left, settingsTabs[1]); + ragdollButtonBox.Selected = AllowRagdollButton; + ragdollButtonBox.OnSelected = (GUITickBox) => + { + AllowRagdollButton = GUITickBox.Selected; + return true; + }; + + y += 20; + + var traitorRatioBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Use % of players for max traitors", Alignment.Left, settingsTabs[1]); + var traitorRatioText = new GUITextBlock(new Rectangle(20, y + 20, 20, 20), "Traitor ratio: 20 %", "", settingsTabs[1], GUI.SmallFont); + var traitorRatioSlider = new GUIScrollBar(new Rectangle(150, y + 22, 100, 15), "", 0.1f, settingsTabs[1]); + //Prepare the slider before the tick box + if (TraitorUseRatio) + { + traitorRatioSlider.UserData = traitorRatioText; + traitorRatioSlider.Step = 0.01f; //Lots of fine-tuning + traitorRatioSlider.BarScroll = (TraitorRatio - 0.1f) / 0.9f; + } + else + { + traitorRatioSlider.UserData = traitorRatioText; + traitorRatioSlider.Step = 1f / (maxPlayers-1); + traitorRatioSlider.BarScroll = MathUtils.Round(TraitorRatio, 1f); + } + //Slider END + + traitorRatioBox.Selected = TraitorUseRatio; + traitorRatioBox.OnSelected = (GUITickBox) => + { + TraitorUseRatio = GUITickBox.Selected; + //Affect the slider graphics + if (TraitorUseRatio) + { + traitorRatioSlider.UserData = traitorRatioText; + traitorRatioSlider.Step = 0.01f; //Lots of fine-tuning + traitorRatioSlider.BarScroll = 0.2f; //default values + traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); //Update the scroll bar + } + else + { + traitorRatioSlider.UserData = traitorRatioText; + traitorRatioSlider.Step = 1f / (maxPlayers-1); + traitorRatioSlider.BarScroll = 1; //default values + traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); //Update the scroll bar + } + return true; + }; + traitorRatioSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock traitorText = scrollBar.UserData as GUITextBlock; + if (TraitorUseRatio) + { + TraitorRatio = barScroll * 0.9f + 0.1f; + traitorText.Text = "Traitor ratio: " + (int)MathUtils.Round(TraitorRatio * 100.0f, 1.0f) + " %"; + } + else + { + TraitorRatio = MathUtils.Round(barScroll * (maxPlayers-1), 1f) + 1; + traitorText.Text = "Traitor count: " + TraitorRatio; + } + return true; + }; + traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); + + y += 45; + + var karmaButtonBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Use Karma", Alignment.Left, settingsTabs[1]); + karmaButtonBox.Selected = KarmaEnabled; + karmaButtonBox.OnSelected = (GUITickBox) => + { + KarmaEnabled = GUITickBox.Selected; + return true; + }; //-------------------------------------------------------------------------------- // banlist diff --git a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs index 16d515bc2..16a7277ad 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs @@ -882,24 +882,54 @@ namespace Barotrauma playerFrame = new GUIFrame(new Rectangle(0, 0, 0, 0), Color.Black * 0.6f); - var playerFrameInner = new GUIFrame(new Rectangle(0, 0, 300, 280), null, Alignment.Center, "", playerFrame); + var playerFrameInner = new GUIFrame(GameMain.Server != null ? new Rectangle(0, 0, 450, 370) : new Rectangle(0, 0, 450, 150), null, Alignment.Center, "", playerFrame); playerFrameInner.Padding = new Vector4(20.0f, 20.0f, 20.0f, 20.0f); - new GUITextBlock(new Rectangle(0, 0, 200, 20), component.UserData.ToString(), + new GUITextBlock(new Rectangle(0, 0, 200, 20), obj.ToString(), "", Alignment.TopLeft, Alignment.TopLeft, playerFrameInner, false, GUI.LargeFont); if (GameMain.Server != null) { - var selectedClient = GameMain.Server.ConnectedClients.Find(c => c.Name == component.UserData.ToString()); + var selectedClient = GameMain.Server.ConnectedClients.Find(c => c.Name == obj.ToString()); + playerFrame.UserData = selectedClient; new GUITextBlock(new Rectangle(0, 25, 150, 15), selectedClient.Connection.RemoteEndPoint.Address.ToString(), "", playerFrameInner); - var permissionsBox = new GUIFrame(new Rectangle(0, 60, 0, 90), null, playerFrameInner); + new GUITextBlock(new Rectangle(0, 45, 0, 15), "Rank", "", playerFrameInner); + var rankDropDown = new GUIDropDown(new Rectangle(0, 70, 150, 20), "Rank", "", playerFrameInner); + rankDropDown.UserData = selectedClient; + foreach (PermissionPreset permissionPreset in PermissionPreset.List) + { + rankDropDown.AddItem(permissionPreset.Name, permissionPreset, permissionPreset.Description); + } + rankDropDown.AddItem("Custom", null); + + PermissionPreset currentPreset = PermissionPreset.List.Find(p => + p.Permissions == selectedClient.Permissions && + p.PermittedCommands.Count == selectedClient.PermittedConsoleCommands.Count && !p.PermittedCommands.Except(selectedClient.PermittedConsoleCommands).Any()); + rankDropDown.SelectItem(currentPreset); + + rankDropDown.OnSelected += (c, userdata) => + { + PermissionPreset selectedPreset = (PermissionPreset)userdata; + if (selectedPreset != null) + { + var client = playerFrame.UserData as Client; + client.SetPermissions(selectedPreset.Permissions, selectedPreset.PermittedCommands); + GameMain.Server.UpdateClientPermissions(client); + + playerFrame = null; + SelectPlayer(null, client.Name); + } + return true; + }; + + var permissionsBox = new GUIFrame(new Rectangle(0, 125, (int)(playerFrameInner.Rect.Width * 0.5f), 160), null, playerFrameInner); permissionsBox.Padding = new Vector4(5.0f, 5.0f, 5.0f, 5.0f); permissionsBox.UserData = selectedClient; - new GUITextBlock(new Rectangle(0, 0, 0, 15), "Permissions:", "", permissionsBox); + new GUITextBlock(new Rectangle(0, 100, permissionsBox.Rect.Width, 15), "Permissions:", "", playerFrameInner); int x = 0, y = 0; foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { @@ -910,13 +940,16 @@ namespace Barotrauma string permissionStr = attributes.Length > 0 ? attributes[0].Description : permission.ToString(); - var permissionTick = new GUITickBox(new Rectangle(x, y + 25, 15, 15), permissionStr, Alignment.TopLeft, GUI.SmallFont, permissionsBox); + var permissionTick = new GUITickBox(new Rectangle(x, y, 15, 15), permissionStr, Alignment.TopLeft, GUI.SmallFont, permissionsBox); permissionTick.UserData = permission; permissionTick.Selected = selectedClient.HasPermission(permission); permissionTick.OnSelected = (tickBox) => { - var client = tickBox.Parent.UserData as Client; + //reset rank to custom + rankDropDown.SelectItem(null); + + var client = playerFrame.UserData as Client; if (client == null) return false; var thisPermission = (ClientPermissions)tickBox.UserData; @@ -931,19 +964,50 @@ namespace Barotrauma return true; }; - y += 20; - if (y >= permissionsBox.Rect.Height - 40) + if (y >= permissionsBox.Rect.Height - 15) { y = 0; - x += 100; + x += 120; } } + + new GUITextBlock(new Rectangle(0, 100, (int)(playerFrameInner.Rect.Width * 0.5f), 15), "Permitted console commands:", "", Alignment.TopRight, Alignment.TopLeft, playerFrameInner, true); + var commandList = new GUIListBox(new Rectangle(0, 125, (int)(playerFrameInner.Rect.Width * 0.5f), 160), "", Alignment.TopRight, playerFrameInner); + commandList.UserData = selectedClient; + foreach (DebugConsole.Command command in DebugConsole.Commands) + { + var commandTickBox = new GUITickBox(new Rectangle(0, 0, 15, 15), command.names[0], Alignment.TopLeft, GUI.SmallFont, commandList); + commandTickBox.Selected = selectedClient.PermittedConsoleCommands.Contains(command); + commandTickBox.ToolTip = command.help; + commandTickBox.UserData = command; + commandTickBox.OnSelected += (GUITickBox tickBox) => + { + //reset rank to custom + rankDropDown.SelectItem(null); + + Client client = playerFrame.UserData as Client; + DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; + if (client == null) return false; + + if (!tickBox.Selected) + { + client.PermittedConsoleCommands.Remove(selectedCommand); + } + else if (!client.PermittedConsoleCommands.Contains(selectedCommand)) + { + client.PermittedConsoleCommands.Add(selectedCommand); + } + + GameMain.Server.UpdateClientPermissions(client); + return true; + }; + } } if (GameMain.Server != null || GameMain.Client.HasPermission(ClientPermissions.Kick)) { - var kickButton = new GUIButton(new Rectangle(0, -50, 100, 20), "Kick", Alignment.BottomLeft, "", playerFrameInner); + var kickButton = new GUIButton(new Rectangle(0, 0, 80, 20), "Kick", Alignment.BottomLeft, "", playerFrameInner); kickButton.UserData = obj; kickButton.OnClicked += KickPlayer; kickButton.OnClicked += ClosePlayerFrame; @@ -951,12 +1015,12 @@ namespace Barotrauma if (GameMain.Server != null || GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Ban", Alignment.BottomLeft, "", playerFrameInner); + var banButton = new GUIButton(new Rectangle(90, 0, 80, 20), "Ban", Alignment.BottomLeft, "", playerFrameInner); banButton.UserData = obj; banButton.OnClicked += BanPlayer; banButton.OnClicked += ClosePlayerFrame; - var rangebanButton = new GUIButton(new Rectangle(0, -25, 100, 20), "Ban range", Alignment.BottomLeft, "", playerFrameInner); + var rangebanButton = new GUIButton(new Rectangle(180, 0, 80, 20), "Ban range", Alignment.BottomLeft, "", playerFrameInner); rangebanButton.UserData = obj; rangebanButton.OnClicked += BanPlayerRange; rangebanButton.OnClicked += ClosePlayerFrame; diff --git a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs index 948deee36..ae33baf0d 100644 --- a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -16,7 +15,7 @@ namespace Barotrauma { while (QueuedCommands.Count>0) { - ExecuteCommand(QueuedCommands[0], GameMain.Instance); + ExecuteCommand(QueuedCommands[0]); QueuedCommands.RemoveAt(0); } } diff --git a/Barotrauma/BarotraumaShared/BarotraumaShared.projitems b/Barotrauma/BarotraumaShared/BarotraumaShared.projitems index 164bdd3a4..c89c3914b 100644 --- a/Barotrauma/BarotraumaShared/BarotraumaShared.projitems +++ b/Barotrauma/BarotraumaShared/BarotraumaShared.projitems @@ -745,6 +745,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -1509,6 +1512,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Content/Items/Jobgear/misc.xml b/Barotrauma/BarotraumaShared/Content/Items/Jobgear/misc.xml index bd07f13ec..8ba8c006e 100644 --- a/Barotrauma/BarotraumaShared/Content/Items/Jobgear/misc.xml +++ b/Barotrauma/BarotraumaShared/Content/Items/Jobgear/misc.xml @@ -14,13 +14,16 @@ - - - - + + + + + + + + - diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml new file mode 100644 index 000000000..e909124d0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs index 1ba1069c6..3e0f20f84 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs @@ -64,7 +64,7 @@ namespace Barotrauma protected override Vector2 DoSteeringSeek(Vector2 target, float speed = 1) { //find a new path if one hasn't been found yet or the target is different from the current target - if (currentPath == null || Vector2.Distance(target, currentTarget)>1.0f || findPathTimer < -5.0f) + if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f) { if (findPathTimer > 0.0f) return Vector2.Zero; @@ -73,21 +73,21 @@ namespace Barotrauma if (character != null && character.Submarine == null) { var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false); - if (targetHull!=null && targetHull.Submarine != null) + if (targetHull != null && targetHull.Submarine != null) { pos -= targetHull.SimPosition; } - } + } currentPath = pathFinder.FindPath(pos, target); - findPathTimer = Rand.Range(1.0f,1.2f); + findPathTimer = Rand.Range(1.0f, 1.2f); return DiffToCurrentNode(); } - + Vector2 diff = DiffToCurrentNode(); - + var collider = character.AnimController.Collider; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically if (!character.AnimController.InWater && @@ -104,7 +104,7 @@ namespace Barotrauma private Vector2 DiffToCurrentNode() { - if (currentPath == null || currentPath.Finished || currentPath.Unreachable) return Vector2.Zero; + if (currentPath == null || currentPath.Unreachable) return Vector2.Zero; if (currentPath.Finished) { @@ -113,8 +113,8 @@ namespace Barotrauma { //todo: take multiple subs into account pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; - } - return currentTarget-pos2; + } + return currentTarget - pos2; } if (canOpenDoors && !character.LockHands) CheckDoorsInPath(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs index ee6381c50..0e527227e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs @@ -4,21 +4,13 @@ using System.Linq; namespace Barotrauma { - class AIObjective + abstract class AIObjective { - protected List subObjectives; - + protected readonly List subObjectives; protected float priority; - - protected Character character; - + protected readonly Character character; protected string option; - public virtual bool IsCompleted() - { - return false; - } - public virtual bool CanBeCompleted { get { return true; } @@ -27,15 +19,12 @@ namespace Barotrauma public string Option { get { return option; } - } - + } public AIObjective(Character character, string option) { subObjectives = new List(); - this.character = character; - this.option = option; #if DEBUG @@ -60,8 +49,6 @@ namespace Barotrauma Act(deltaTime); } - protected virtual void Act(float deltaTime) { } - public void AddSubObjective(AIObjective objective) { if (subObjectives.Any(o => o.IsDuplicate(objective))) return; @@ -69,19 +56,20 @@ namespace Barotrauma subObjectives.Add(objective); } - public virtual float GetPriority(Character character) + public AIObjective GetCurrentSubObjective() { - return 0.0f; + AIObjective currentSubObjective = this; + while (currentSubObjective.subObjectives.Count > 0) + { + currentSubObjective = subObjectives[0]; + } + return currentSubObjective; } - public virtual bool IsDuplicate(AIObjective otherObjective) - { -#if DEBUG - throw new NotImplementedException(); -#else - return (this.GetType() == otherObjective.GetType()); -#endif - - } + protected abstract void Act(float deltaTime); + + public abstract bool IsCompleted(); + public abstract float GetPriority(AIObjectiveManager objectiveManager); + public abstract bool IsDuplicate(AIObjective otherObjective); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs index cbe66e791..cf84ae4f3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -33,7 +33,6 @@ namespace Barotrauma } coolDownTimer = CoolDown; - } protected override void Act(float deltaTime) @@ -41,17 +40,24 @@ namespace Barotrauma coolDownTimer -= deltaTime; var weapon = character.Inventory.FindItem("weapon"); - - if (weapon==null) + + if (weapon == null) { Escape(deltaTime); } else { + //TODO: make sure the weapon is ready to use (projectiles/batteries loaded) if (!character.SelectedItems.Contains(weapon)) { - character.Inventory.TryPutItem(weapon, 3, false, character); - weapon.Equip(character); + if (character.Inventory.TryPutItem(weapon, 3, false, character)) + { + weapon.Equip(character); + } + else + { + return; + } } character.CursorPosition = enemy.Position; character.SetInput(InputType.Aim, false, true); @@ -99,8 +105,13 @@ namespace Barotrauma return enemy.IsDead || coolDownTimer <= 0.0f; } - public override float GetPriority(Character character) + public override float GetPriority(AIObjectiveManager objectiveManager) { + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + //clamp the strength to the health of this character //(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs index bc92c927e..9dc4e26bc 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -19,15 +19,6 @@ namespace Barotrauma { this.itemName = itemName; this.container = container; - - //check if the container has room for more items - //canBeCompleted = false; - //foreach (Item contained in container.inventory.Items) - //{ - // if (contained != null) continue; - // canBeCompleted = true; - // break; - //} } public override bool IsCompleted() @@ -35,6 +26,16 @@ namespace Barotrauma return isCompleted || container.Inventory.FindItem(itemName)!=null; } + public override float GetPriority(AIObjectiveManager objectiveManager) + { + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + + return 1.0f; + } + protected override void Act(float deltaTime) { if (isCompleted) return; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 0b9928d66..132408dd3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -15,7 +15,7 @@ namespace Barotrauma if (item == null) return false; var containedItems = item.ContainedItems; - var oxygenTank = Array.Find(containedItems, i => i.Name == "Oxygen Tank" && i.Condition > 0.0f); + var oxygenTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Oxygen Tank") && i.Condition > 0.0f); return oxygenTank != null; } @@ -42,7 +42,7 @@ namespace Barotrauma if (containedItems == null) return; //check if there's an oxygen tank in the mask - var oxygenTank = Array.Find(containedItems, i => i.Name == "Oxygen Tank"); + var oxygenTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Oxygen Tank")); if (oxygenTank != null) { @@ -66,15 +66,18 @@ namespace Barotrauma if (subObjective != null) { subObjective.TryComplete(deltaTime); - - //isCompleted = subObjective.IsCompleted(); } } - public override float GetPriority(Character character) + public override float GetPriority(AIObjectiveManager objectiveManager) { if (character.AnimController.CurrentHull == null) return 100.0f; + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + return 100.0f - character.Oxygen; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 56de5b8e5..391900d20 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -27,39 +28,23 @@ namespace Barotrauma unreachable = new List(); } + public override bool IsCompleted() + { + return false; + } + protected override void Act(float deltaTime) { - var currentHull = character.AnimController.CurrentHull; currenthullSafety = OverrideCurrentHullSafety == null ? GetHullSafety(currentHull, character) : (float)OverrideCurrentHullSafety; - - if (currentHull != null) + + if (NeedsDivingGear()) { - if (NeedsDivingGear()) - { - if (!FindDivingGear(deltaTime)) return; - } - - if (currenthullSafety > MinSafety) - { - if (Math.Abs(currentHull.WorldPosition.X - character.WorldPosition.X) > 100.0f) - { - character.AIController.SteeringManager.SteeringSeek(currentHull.SimPosition, 0.5f); - } - else - { - - character.AIController.SteeringManager.Reset(); - } - - character.AIController.SelectTarget(null); - - goToObjective = null; - return; - } + if (!FindDivingGear(deltaTime)) return; } + if (searchHullTimer > 0.0f) { @@ -79,7 +64,7 @@ namespace Barotrauma if (goToObjective != null) { var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager; - if (pathSteering!=null && pathSteering.CurrentPath!= null && + if (pathSteering != null && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target)) { unreachable.Add(goToObjective.Target as Hull); @@ -92,7 +77,7 @@ namespace Barotrauma private bool FindDivingGear(float deltaTime) { - if (divingGearObjective==null) + if (divingGearObjective == null) { divingGearObjective = new AIObjectiveFindDivingGear(character, false); } @@ -113,8 +98,9 @@ namespace Barotrauma if (hull == character.AnimController.CurrentHull || unreachable.Contains(hull)) continue; float hullValue = GetHullSafety(hull, character); - hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.X - hull.Position.X)); - hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.Y - hull.Position.Y) * 2.0f); + //slight preference over hulls that are closer + hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.X - hull.Position.X)) * 0.1f; + hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.Y - hull.Position.Y)) * 0.2f; if (bestHull == null || hullValue > bestValue) { @@ -144,13 +130,13 @@ namespace Barotrauma return false; } - public override float GetPriority(Character character) + public override float GetPriority(AIObjectiveManager objectiveManager) { if (character.Oxygen < 80.0f) { return 150.0f - character.Oxygen; } - + if (character.AnimController.CurrentHull == null) return 5.0f; currenthullSafety = GetHullSafety(character.AnimController.CurrentHull, character); priority = 100.0f - currenthullSafety; @@ -171,13 +157,12 @@ namespace Barotrauma } } } - - + if (NeedsDivingGear()) { if (divingGearObjective != null && !divingGearObjective.IsCompleted()) priority += 20.0f; } - + return priority; } @@ -185,11 +170,28 @@ namespace Barotrauma { if (hull == null) return 0.0f; + float safety = 100.0f; + float waterPercentage = (hull.WaterVolume / hull.Volume) * 100.0f; + if (hull.LethalPressure > 0.0f && character.PressureProtection <= 0.0f) + { + safety -= 100.0f; + } + else if (character.OxygenAvailable <= 0.0f) + { + safety -= waterPercentage; + } + else + { + safety -= waterPercentage * 0.1f; + } + + if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f; + + if (safety <= 0.0f) return 0.0f; + float fireAmount = 0.0f; - var nearbyHulls = hull.GetConnectedHulls(3); - foreach (Hull hull2 in nearbyHulls) { foreach (FireSource fireSource in hull2.FireSources) @@ -204,13 +206,9 @@ namespace Barotrauma } } } + safety -= fireAmount; - float safety = 100.0f - fireAmount; - - if (waterPercentage > 30.0f && character.OxygenAvailable <= 0.0f) safety -= waterPercentage; - if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f; - - return safety; + return MathHelper.Clamp(safety, 0.0f, 100.0f); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 356ed3590..f1e9bfd97 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -7,7 +7,9 @@ namespace Barotrauma { class AIObjectiveFixLeak : AIObjective { - private Gap leak; + private readonly Gap leak; + + private AIObjectiveGoTo gotoObjective; public Gap Leak { @@ -20,15 +22,20 @@ namespace Barotrauma this.leak = leak; } - public override float GetPriority(Character character) + public override bool IsCompleted() + { + return leak.Open <= 0.0f || leak.Removed; + } + + public override float GetPriority(AIObjectiveManager objectiveManager) { if (leak.Open == 0.0f) return 0.0f; float leakSize = (leak.IsHorizontal ? leak.Rect.Height : leak.Rect.Width) * Math.Max(leak.Open, 0.1f); float dist = Vector2.DistanceSquared(character.SimPosition, leak.SimPosition); - dist = Math.Max(dist/100.0f, 1.0f); - return Math.Min(leakSize/dist, 40.0f); + dist = Math.Max(dist / 100.0f, 1.0f); + return Math.Min(leakSize / dist, 40.0f); } public override bool IsDuplicate(AIObjective otherObjective) @@ -44,7 +51,7 @@ namespace Barotrauma if (weldingTool == null) { - subObjectives.Add(new AIObjectiveGetItem(character, "Welding Tool", true)); + AddSubObjective(new AIObjectiveGetItem(character, "Welding Tool", true)); return; } else @@ -52,25 +59,31 @@ namespace Barotrauma var containedItems = weldingTool.ContainedItems; if (containedItems == null) return; - var fuelTank = Array.Find(containedItems, i => i.Name == "Welding Fuel Tank" && i.Condition > 0.0f); + var fuelTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Welding Fuel Tank") && i.Condition > 0.0f); if (fuelTank == null) { AddSubObjective(new AIObjectiveContainItem(character, "Welding Fuel Tank", weldingTool.GetComponent())); + return; } } var repairTool = weldingTool.GetComponent(); if (repairTool == null) return; - if (Vector2.Distance(character.WorldPosition, leak.WorldPosition) > 300.0f) + Vector2 standPosition = GetStandPosition(); + + if (Vector2.DistanceSquared(character.WorldPosition, leak.WorldPosition) > 100.0f * 100.0f) { - AddSubObjective(new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character)); + var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character); + if (!gotoObjective.IsCompleted()) + { + AddSubObjective(gotoObjective); + return; + } } - else - { - AddSubObjective(new AIObjectiveOperateItem(repairTool, character, "", leak)); - } + + AddSubObjective(new AIObjectiveOperateItem(repairTool, character, "", true, leak)); } private Vector2 GetStandPosition() diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index f29971f03..2414a726c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -7,38 +7,80 @@ namespace Barotrauma { class AIObjectiveFixLeaks : AIObjective { - const float UpdateGapListInterval = 10.0f; + const float UpdateGapListInterval = 5.0f; - private float updateGapListTimer; + private double lastGapUpdate; private AIObjectiveIdle idleObjective; + private AIObjectiveFindDivingGear findDivingGear; + private List objectiveList; public AIObjectiveFixLeaks(Character character) : base (character, "") { - objectiveList = new List(); } public override bool IsCompleted() { - return false; + if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null) + { + UpdateGapList(); + lastGapUpdate = Timing.TotalTime; + } + + return objectiveList.Count == 0; + } + + public override float GetPriority(AIObjectiveManager objectiveManager) + { + if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null) + { + UpdateGapList(); + lastGapUpdate = Timing.TotalTime; + } + + float priority = 0.0f; + foreach (AIObjectiveFixLeak fixObjective in objectiveList) + { + //gaps from outside to inside significantly increase the priority + if (!fixObjective.Leak.IsRoomToRoom) + { + priority = Math.Max(priority + fixObjective.Leak.Open * 100.0f, 50.0f); + } + else + { + priority += fixObjective.Leak.Open * 10.0f; + } + + if (priority >= 100.0f) break; + } + + return Math.Min(priority, 100.0f); } protected override void Act(float deltaTime) { - updateGapListTimer -= deltaTime; - - if (updateGapListTimer<=0.0f) + if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null) { UpdateGapList(); - - updateGapListTimer = UpdateGapListInterval; + lastGapUpdate = Timing.TotalTime; } if (objectiveList.Any()) { + if (!objectiveList[objectiveList.Count - 1].Leak.IsRoomToRoom) + { + if (findDivingGear == null) findDivingGear = new AIObjectiveFindDivingGear(character, true); + + if (!findDivingGear.IsCompleted() && findDivingGear.CanBeCompleted) + { + findDivingGear.TryComplete(deltaTime); + return; + } + } + objectiveList[objectiveList.Count - 1].TryComplete(deltaTime); if (!objectiveList[objectiveList.Count - 1].CanBeCompleted || @@ -56,6 +98,7 @@ namespace Barotrauma private void UpdateGapList() { + if (objectiveList == null) objectiveList = new List(); objectiveList.Clear(); foreach (Gap gap in Gap.GapList) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs index 07aec2f29..05c80fa69 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using System; namespace Barotrauma { @@ -26,6 +27,16 @@ namespace Barotrauma get { return canBeCompleted; } } + public override float GetPriority(AIObjectiveManager objectiveManager) + { + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + + return 1.0f; + } + public AIObjectiveGetItem(Character character, Item targetItem, bool equip = false) : base(character, "") { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs index 3a9b2e037..cf566bd8a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -18,6 +18,16 @@ namespace Barotrauma private bool getDivingGearIfNeeded; + public override float GetPriority(AIObjectiveManager objectiveManager) + { + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + + return 1.0f; + } + public override bool CanBeCompleted { get @@ -93,7 +103,7 @@ namespace Barotrauma } } - if (Vector2.Distance(currTargetPos, character.SimPosition) < 1.0f) + if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < 0.5f * 0.5f) { character.AIController.SteeringManager.Reset(); character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left; @@ -104,7 +114,7 @@ namespace Barotrauma var indoorsSteering = character.AIController.SteeringManager as IndoorsSteeringManager; - if (indoorsSteering.CurrentPath==null || indoorsSteering.CurrentPath.Unreachable) + if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable) { indoorsSteering.SteeringWander(); } @@ -130,7 +140,7 @@ namespace Barotrauma if (item.IsInsideTrigger(character.WorldPosition)) completed = true; } - completed = completed || Vector2.Distance(target != null ? target.SimPosition : targetPos, character.SimPosition) < allowedDistance; + completed = completed || Vector2.DistanceSquared(target != null ? target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance; if (completed) character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs index 59b5b6f6a..6d0c6df9f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -9,15 +9,21 @@ namespace Barotrauma { const float WallAvoidDistance = 150.0f; - AITarget currentTarget; + private AITarget currentTarget; private float newTargetTimer; + private AIObjectiveFindSafety findSafety; + public AIObjectiveIdle(Character character) : base(character, "") { - } - public override float GetPriority(Character character) + public override bool IsCompleted() + { + return false; + } + + public override float GetPriority(AIObjectiveManager objectiveManager) { return 1.0f; } @@ -31,6 +37,14 @@ namespace Barotrauma { return; } + + if (character.AnimController.InWater) + { + //attempt to find a safer place if in water + if (findSafety == null) findSafety = new AIObjectiveFindSafety(character); + findSafety.TryComplete(deltaTime); + return; + } if (newTargetTimer <= 0.0f) { @@ -92,17 +106,10 @@ namespace Barotrauma return; } } - - if (character.AnimController.InWater) - { - character.AIController.SteeringManager.SteeringManual(deltaTime, new Vector2(0.0f, 0.5f)); - } - else - { - character.AIController.SteeringManager.SteeringWander(); - //reset vertical steering to prevent dropping down from platforms etc - character.AIController.SteeringManager.ResetY(); - } + + character.AIController.SteeringManager.SteeringWander(); + //reset vertical steering to prevent dropping down from platforms etc + character.AIController.SteeringManager.ResetY(); return; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs index 8482e3867..468434e8c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs @@ -11,15 +11,17 @@ namespace Barotrauma private Character character; - private AIObjective currentObjective; + private AIObjective currentOrder; + public AIObjective CurrentOrder + { + get { return currentOrder; } + } + public AIObjective CurrentObjective { - get - { - if (currentObjective != null) return currentObjective; - return objectives.Any() ? objectives[0] : null; - } + get; + private set; } public AIObjectiveManager(Character character) @@ -47,8 +49,13 @@ namespace Barotrauma public float GetCurrentPriority(Character character) { - if (currentObjective != null) return OrderPriority; - return (CurrentObjective == null) ? 0.0f : CurrentObjective.GetPriority(character); + if (CurrentOrder != null && + (objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this))) + { + return CurrentOrder.GetPriority(this); + } + + return objectives.Count == 0 ? 0.0f : objectives[0].GetPriority(this); } public void UpdateObjectives() @@ -59,43 +66,46 @@ namespace Barotrauma objectives = objectives.FindAll(o => !o.IsCompleted() && o.CanBeCompleted); //sort objectives according to priority - objectives.Sort((x, y) => y.GetPriority(character).CompareTo(x.GetPriority(character))); + objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this))); } public void DoCurrentObjective(float deltaTime) { - if (currentObjective != null && (!objectives.Any() || objectives[0].GetPriority(character) < OrderPriority)) + if (currentOrder != null && (!objectives.Any() || objectives[0].GetPriority(this) < currentOrder.GetPriority(this))) { - currentObjective.TryComplete(deltaTime); + CurrentObjective = currentOrder; + currentOrder.TryComplete(deltaTime); return; } if (!objectives.Any()) return; objectives[0].TryComplete(deltaTime); + + CurrentObjective = objectives[0]; } public void SetOrder(Order order, string option) { if (order == null) return; - currentObjective = null; + currentOrder = null; switch (order.Name.ToLowerInvariant()) { case "follow": - currentObjective = new AIObjectiveGoTo(Character.Controlled, character, true); + currentOrder = new AIObjectiveGoTo(Character.Controlled, character, true); break; case "wait": - currentObjective = new AIObjectiveGoTo(character, character, true); + currentOrder = new AIObjectiveGoTo(character, character, true); break; case "fixleaks": case "fix leaks": - currentObjective = new AIObjectiveFixLeaks(character); + currentOrder = new AIObjectiveFixLeaks(character); break; default: if (order.TargetItem == null) return; - currentObjective = new AIObjectiveOperateItem(order.TargetItem, character, option, null, order.UseController); + currentOrder = new AIObjectiveOperateItem(order.TargetItem, character, option, false, null, order.UseController); break; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 9788393a3..061442e73 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -14,6 +15,8 @@ namespace Barotrauma private bool canBeCompleted; + private bool requireEquip; + public override bool CanBeCompleted { get @@ -27,11 +30,21 @@ namespace Barotrauma get { return operateTarget; } } - public AIObjectiveOperateItem(ItemComponent item, Character character, string option, Entity operateTarget = null, bool useController = false) - :base (character, option) + public override float GetPriority(AIObjectiveManager objectiveManager) + { + if (objectiveManager.CurrentOrder == this) + { + return AIObjectiveManager.OrderPriority; + } + + return 1.0f; + } + + public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false) + : base (character, option) { this.component = item; - + this.requireEquip = requireEquip; this.operateTarget = operateTarget; if (useController) @@ -72,6 +85,43 @@ namespace Barotrauma } else { + if (requireEquip && !character.HasEquippedItem(component.Item)) + { + //the item has to be equipped before using it if it's holdable + var holdable = component.Item.GetComponent(); + if (holdable == null) + { + DebugConsole.ThrowError("AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component"); + return; + } + + for (int i = 0; i < CharacterInventory.limbSlots.Length; i++) + { + if (CharacterInventory.limbSlots[i] == InvSlotType.Any || + !holdable.AllowedSlots.Any(s => s.HasFlag(CharacterInventory.limbSlots[i]))) + { + continue; + } + + //equip slot already taken + if (character.Inventory.Items[i] != null) + { + //try to put the item in an Any slot, and drop it if that fails + if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) || + !character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List() { InvSlotType.Any })) + { + character.Inventory.Items[i].Drop(); + } + } + if (character.Inventory.TryPutItem(component.Item, i, true, character)) + { + component.Item.Equip(character); + break; + } + } + return; + } + if (component.AIOperate(deltaTime, character, this)) isCompleted = true; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs index 84f291d48..60391493f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Barotrauma { - class AIObjectiveRescue : AIObjective + /*class AIObjectiveRescue : AIObjective { private readonly Character targetCharacter; @@ -30,5 +30,5 @@ namespace Barotrauma return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance; } - } + }*/ } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs index adeb631b9..784b76859 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Barotrauma { - class AIObjectiveRescueAll : AIObjective + /*class AIObjectiveRescueAll : AIObjective { private List rescueTargets; @@ -44,5 +44,5 @@ namespace Barotrauma AddSubObjective(new AIObjectiveRescue(character, target)); } } - } + }*/ } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs index 28cfc35c4..7fcc25d17 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs @@ -9,6 +9,8 @@ namespace Barotrauma public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR }; public Animation Anim; + public LimbType GrabLimb; + protected Character character; protected float walkSpeed, swimSpeed; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs index 9dbaf2193..dd7d675c5 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs @@ -20,6 +20,7 @@ namespace Barotrauma private float thighTorque; private float cprAnimState; + private float cprPump; private float inWaterTimer; private bool swimming; @@ -872,7 +873,34 @@ namespace Barotrauma character.SelectedConstruction = null; IgnorePlatforms = false; } + else if (character.SelectedCharacter != null && !character.SelectedCharacter.AllowInput) + { + Limb targetLeftHand = character.SelectedCharacter.AnimController.GetLimb(LimbType.LeftHand); + Limb targetRightHand = character.SelectedCharacter.AnimController.GetLimb(LimbType.RightHand); + Limb targetTorso = character.SelectedCharacter.AnimController.GetLimb(LimbType.Torso); + if (character.SelectedCharacter.AnimController.Dir != Dir) + character.SelectedCharacter.AnimController.Flip(); + + targetTorso.pullJoint.Enabled = true; + targetTorso.pullJoint.WorldAnchorB = torso.SimPosition + (Vector2.UnitX * -Dir) * 0.2f; + targetTorso.pullJoint.MaxForce = 5000.0f; + + if (!targetLeftHand.IsSevered) + { + targetLeftHand.pullJoint.Enabled = true; + targetLeftHand.pullJoint.WorldAnchorB = torso.SimPosition + (new Vector2(1 * Dir, 1)) * 0.2f; + targetLeftHand.pullJoint.MaxForce = 5000.0f; + } + if (!targetRightHand.IsSevered) + { + targetRightHand.pullJoint.Enabled = true; + targetRightHand.pullJoint.WorldAnchorB = torso.SimPosition + (new Vector2(1 * Dir, 1)) * 0.2f; + targetRightHand.pullJoint.MaxForce = 5000.0f; + } + + character.SelectedCharacter.AnimController.IgnorePlatforms = true; + } } private void UpdateCPR(float deltaTime) @@ -883,11 +911,16 @@ namespace Barotrauma return; } + Character target = character.SelectedCharacter; + Crouching = true; - Vector2 diff = character.SelectedCharacter.SimPosition - character.SimPosition; - var targetHead = character.SelectedCharacter.AnimController.GetLimb(LimbType.Head); - + Vector2 diff = target.SimPosition - character.SimPosition; + Limb targetHead = target.AnimController.GetLimb(LimbType.Head); + Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); + Limb head = GetLimb(LimbType.Head); + Limb torso = GetLimb(LimbType.Torso); + Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition; targetMovement = new Vector2(diff.X, 0.0f); @@ -895,21 +928,98 @@ namespace Barotrauma UpdateStanding(); - Vector2 handPos = character.SelectedCharacter.AnimController.GetLimb(LimbType.Torso).SimPosition + Vector2.UnitY * 0.2f; + Vector2 handPos = targetTorso.SimPosition + Vector2.UnitY * 0.2f; Grab(handPos, handPos); - float yPos = (float)Math.Sin(cprAnimState) * 0.1f; - cprAnimState += deltaTime * 8.0f; + Vector2 colliderPos = GetColliderBottom(); - var head = GetLimb(LimbType.Head); - head.pullJoint.WorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.6f + yPos); - head.pullJoint.Enabled = true; + if (GameMain.Client == null) //Serverside code + { + if (target.Bleeding <= 0.5f && target.Oxygen <= 0.0f) //If they're bleeding too hard CPR will hurt them + { + target.Oxygen += deltaTime * 0.5f; //Stabilize them + } + } + + + int skill = character.GetSkillLevel("Medical"); + if (cprAnimState % 17 > 15.0f) + { + float yPos = (float)Math.Sin(cprAnimState) * 0.2f; + head.pullJoint.WorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.3f + yPos); + head.pullJoint.Enabled = true; + torso.pullJoint.WorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition - 0.2f)); + torso.pullJoint.Enabled = true; + + if (GameMain.Client == null) //Serverside code + { + float cpr = skill / 2.0f; //Max possible oxygen addition is 20 per second + character.Oxygen -= (30.0f - cpr) * deltaTime; //Worse skill = more oxygen required + if (character.Oxygen > 0.0f) //we didn't suffocate yet did we + target.Oxygen += cpr * deltaTime; + + //DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua); + } + } + else + { + head.pullJoint.WorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f); + head.pullJoint.Enabled = true; + torso.pullJoint.WorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition - 0.1f)); + torso.pullJoint.Enabled = true; + if (cprPump >= 1) + { + torso.body.ApplyForce(new Vector2(0, -1000f)); + targetTorso.body.ApplyForce(new Vector2(0, -1000f)); + cprPump = 0; + + if (target.Bleeding <= 0.5f && target.Health <= 0.0f && !target.IsDead) //Have a chance to revive them to 2 HP if they were damaged. + { + if (GameMain.Client == null) //Serverside code + { + float reviveChance = (cprAnimState % 17) * (skill / 50.0f); //~5% max chance for 10 skill, ~50% max chance for 100 skill + float rng = Rand.Int(100, Rand.RandSync.Server); + + //DebugConsole.NewMessage("CPR Pump cprAnimState: " + (cprAnimState % 17) + " revive chance: " + reviveChance + " rng: " + rng, Color.Aqua); + if (rng <= reviveChance) //HOLY CRAP YOU SAVED HIM!!! + { + target.Oxygen = Math.Max(target.Oxygen, 10.0f); + target.Health = 2.0f; + Anim = Animation.None; + return; + } + } + } + else if (target.Bleeding > 0.5f || skill < 50) //We will hurt them if they're bleeding or we suck + { + //If not bleeding: 10% skill causes 0.8 damage per pump, 40% skill causes only 0.2 + if (target.Bleeding <= 0.5f) + target.AddDamage(CauseOfDeath.Damage, (50 - skill) * 0.02f, character); + else //If bleeding: 2 HP damage per pump. Basically speeds up their death. Don't pump bleeding people! + { + target.AddDamage(CauseOfDeath.Bloodloss, 1.0f, character); +#if CLIENT + SoundPlayer.PlayDamageSound(DamageSoundType.LimbBlunt, 25.0f, targetTorso.body); + + for (int i = 0; i < 4; i++) + { + var blood = GameMain.ParticleManager.CreateParticle(inWater ? "waterblood" : "blood", targetTorso.WorldPosition, Rand.Vector(10.0f), 0.0f, target.AnimController.CurrentHull); + } +#endif + } + } + } + cprPump += deltaTime; + } + + cprAnimState += deltaTime; } public override void DragCharacter(Character target) { if (target == null) return; + Limb torso = GetLimb(LimbType.Torso); Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); @@ -922,31 +1032,34 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - Limb targetLimb = target.AnimController.GetLimb(LimbType.Torso); + Limb targetLimb = target.AnimController.GetLimb(GrabLimb); - if (i == 0) + if (targetLimb == null || targetLimb.IsSevered) { - if (!targetLeftHand.IsSevered) + targetLimb = target.AnimController.GetLimb(LimbType.Torso); + if (i == 0) { - targetLimb = targetLeftHand; + if (!targetLeftHand.IsSevered) + { + targetLimb = targetLeftHand; + } + else if (!targetRightHand.IsSevered) + { + targetLimb = targetRightHand; + } } - else if (!targetRightHand.IsSevered) + else { - targetLimb = targetRightHand; + if (!targetRightHand.IsSevered) + { + targetLimb = targetRightHand; + } + else if (!targetLeftHand.IsSevered) + { + targetLimb = targetLeftHand; + } } } - else - { - if (!targetRightHand.IsSevered) - { - targetLimb = targetRightHand; - } - else if (!targetLeftHand.IsSevered) - { - targetLimb = targetLeftHand; - } - } - Limb pullLimb = i == 0 ? leftHand : rightHand; if (i == 1 && inWater) @@ -958,12 +1071,32 @@ namespace Barotrauma Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - pullLimb.WorldPosition); pullLimb.pullJoint.Enabled = true; - pullLimb.pullJoint.WorldAnchorB = pullLimb.SimPosition + diff; - pullLimb.pullJoint.MaxForce = 10000.0f; + if (targetLimb.type == LimbType.Torso) + { + pullLimb.pullJoint.WorldAnchorB = targetLimb.SimPosition; + pullLimb.pullJoint.MaxForce = 5000.0f; + targetMovement *= 0.7f; //Carrying people like that takes a lot of effort. + + if (target.AnimController.Dir != Dir) + target.AnimController.Flip(); + } + else + { + pullLimb.pullJoint.WorldAnchorB = pullLimb.SimPosition + diff; + pullLimb.pullJoint.MaxForce = 5000.0f; + } targetLimb.pullJoint.Enabled = true; - targetLimb.pullJoint.WorldAnchorB = targetLimb.SimPosition - diff; - targetLimb.pullJoint.MaxForce = 10000.0f; + if (targetLimb.type == LimbType.Torso) + { + targetLimb.pullJoint.WorldAnchorB = torso.SimPosition + (Vector2.UnitX * Dir) * 0.6f; + targetLimb.pullJoint.MaxForce = 300.0f; + } + else + { + targetLimb.pullJoint.WorldAnchorB = targetLimb.SimPosition - diff; + targetLimb.pullJoint.MaxForce = 5000.0f; + } target.AnimController.movement = -diff; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 9193e6813..0783bbdb6 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -1384,7 +1384,7 @@ namespace Barotrauma findFocusedTimer -= deltaTime; } - if (SelectedCharacter != null && IsKeyHit(InputType.Select)) + if (SelectedCharacter != null && focusedItem == null && IsKeyHit(InputType.Select)) //Let people use ladders and buttons and stuff when dragging chars { DeselectCharacter(); } @@ -1542,26 +1542,42 @@ namespace Barotrauma } } + //Skip health effects as critical health handles it differently if (IsUnconscious) { UpdateUnconscious(deltaTime); return; } + //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! if (IsForceRagdolled) IsRagdolled = IsForceRagdolled; - else if (!IsRagdolled || AnimController.Collider.LinearVelocity.Length() < 1f) //Keep us ragdolled if we were forced or we're too speedy to unragdoll + else if ((GameMain.Server == null || GameMain.Server.AllowRagdollButton) && (!IsRagdolled || AnimController.Collider.LinearVelocity.Length() < 1f)) //Keep us ragdolled if we were forced or we're too speedy to unragdoll IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + //Health effects + if (needsAir) UpdateOxygen(deltaTime); + + Health -= bleeding * deltaTime; + Bleeding -= BleedingDecreaseSpeed * deltaTime; + + if (health <= minHealth) Kill(CauseOfDeath.Bloodloss); + + if (!IsDead) LockHands = false; + + //ragdoll button if (IsRagdolled) { if (AnimController is HumanoidAnimController) ((HumanoidAnimController)AnimController).Crouching = false; - + if(GameMain.Server != null) + GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); AnimController.ResetPullJoints(); selectedConstruction = null; return; } + //AI and control stuff + Control(deltaTime, cam); if (controlled != this && (!(this is AICharacter) || IsRemotePlayer)) { @@ -1574,11 +1590,6 @@ namespace Barotrauma selectedConstruction = null; } - if (SelectedCharacter != null && AnimController.Anim == AnimController.Animation.CPR) - { - if (GameMain.Client == null) SelectedCharacter.Oxygen += (GetSkillLevel("Medical") / 10.0f) * deltaTime; - } - UpdateSightRange(); if (aiTarget != null) aiTarget.SoundRange = 0.0f; @@ -1595,6 +1606,7 @@ namespace Barotrauma if (health <= minHealth) Kill(CauseOfDeath.Bloodloss); if (!IsDead) LockHands = false; + //CPR stuff is handled in the UpdateCPR function in HumanoidAnimController } partial void UpdateControlled(float deltaTime, Camera cam); @@ -1634,12 +1646,17 @@ namespace Barotrauma AnimController.ResetPullJoints(); selectedConstruction = null; - if (oxygen <= 0.0f) Oxygen -= deltaTime * 0.5f; + Oxygen -= deltaTime * 0.5f; //We're critical - our heart stopped! - if (health <= 0.0f) + if (health <= 0.0f) //Critical health - use current state for crit time { AddDamage(bleeding > 0.5f ? CauseOfDeath.Bloodloss : CauseOfDeath.Damage, Math.Max(bleeding, 1.0f) * deltaTime, null); } + else //Keep on bleedin' + { + Health -= bleeding * deltaTime; + Bleeding -= BleedingDecreaseSpeed * deltaTime; + } } private void UpdateSightRange() diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs index 1a6ec6b07..0420bbab4 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs @@ -325,7 +325,7 @@ namespace Barotrauma break; case ClientNetObject.ENTITY_STATE: - int eventType = msg.ReadRangedInteger(0,2); + int eventType = msg.ReadRangedInteger(0,3); switch (eventType) { case 0: @@ -357,6 +357,9 @@ namespace Barotrauma Kill(lastAttackCauseOfDeath); } break; + case 3: + AnimController.GrabLimb = (LimbType)msg.ReadUInt16(); + break; } break; } @@ -436,6 +439,7 @@ namespace Barotrauma if (AnimController is HumanoidAnimController) { tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + tempBuffer.Write((UInt16)AnimController.GrabLimb); } bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null); @@ -534,6 +538,8 @@ namespace Barotrauma msg.WriteRangedSingle(MathHelper.Clamp(Stun, 0.0f, MaxStun), 0.0f, MaxStun, 8); } + msg.Write(IsRagdolled); + msg.Write(HuskInfectionState > 0.0f); } } diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index 5919b05d7..e1216f145 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -27,24 +27,77 @@ namespace Barotrauma static partial class DebugConsole { - class Command + public class Command { public readonly string[] names; public readonly string help; + private Action onExecute; + /// + /// Executed when a client uses the command. If not set, the command is relayed to the server as-is. + /// + private Action onClientExecute; + + /// + /// Executed server-side when a client attempts to use the command. + /// + private Action onClientRequestExecute; + + public bool RelayToServer + { + get { return onClientExecute == null; } + } + + /// The name of the command. Use | to give multiple names/aliases to the command. + /// The text displayed when using the help command. + /// The default action when executing the command. + /// The action when a client attempts to execute the command. If null, the command is relayed to the server as-is. + /// The server-side action when a client requests executing the command. If null, the default action is executed. + public Command(string name, string help, Action onExecute, Action onClientExecute, Action onClientRequestExecute) + { + names = name.Split('|'); + this.help = help; + + this.onExecute = onExecute; + this.onClientExecute = onClientExecute; + this.onClientRequestExecute = onClientRequestExecute; + } + + + /// + /// Use this constructor to create a command that executes the same action regardless of whether it's executed by a client or the server. + /// public Command(string name, string help, Action onExecute) { names = name.Split('|'); this.help = help; this.onExecute = onExecute; + this.onClientExecute = onExecute; } public void Execute(string[] args) { onExecute(args); } + + public void ClientExecute(string[] args) + { + onClientExecute(args); + } + + public void ServerExecuteOnClientRequest(Client client, Vector2 cursorWorldPos, string[] args) + { + if (onClientRequestExecute == null) + { + onExecute(args); + } + else + { + onClientRequestExecute(client, cursorWorldPos, args); + } + } } const int MaxMessages = 200; @@ -53,11 +106,12 @@ namespace Barotrauma public delegate void QuestionCallback(string answer); private static QuestionCallback activeQuestionCallback; -#if CLIENT - private static GUIComponent activeQuestionText; -#endif private static List commands = new List(); + public static List Commands + { + get { return commands; } + } private static string currentAutoCompletedCommand; private static int currentAutoCompletedIndex; @@ -100,6 +154,15 @@ namespace Barotrauma NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.RemoteEndPoint.Address.ToString(), Color.Cyan); } NewMessage("***************", Color.Cyan); + }, null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + GameMain.Server.SendConsoleMessage("***************", client); + foreach (Client c in GameMain.Server.ConnectedClients) + { + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.RemoteEndPoint.Address.ToString(), client); + } + GameMain.Server.SendConsoleMessage("***************", client); })); commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their targets.", (string[] args) => @@ -133,145 +196,42 @@ namespace Barotrauma commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename] [near/inside/outside]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine).", (string[] args) => { - if (args.Length == 0) return; - - Character spawnedCharacter = null; - - Vector2 spawnPosition = Vector2.Zero; - WayPoint spawnPoint = null; - - if (args.Length > 1) + string errorMsg; + SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out errorMsg); + if (!string.IsNullOrWhiteSpace(errorMsg)) { - switch (args[1].ToLowerInvariant()) - { - case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); - break; - case "outside": - spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); - break; - case "near": - case "close": - float closestDist = -1.0f; - foreach (WayPoint wp in WayPoint.WayPointList) - { - if (wp.Submarine != null) continue; - - //don't spawn inside hulls - if (Hull.FindHull(wp.WorldPosition, null) != null) continue; - - float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter); - - if (closestDist < 0.0f || dist < closestDist) - { - spawnPoint = wp; - closestDist = dist; - } - } - break; - case "cursor": - spawnPosition = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - break; - default: - spawnPoint = WayPoint.GetRandom(args[0].ToLowerInvariant() == "human" ? SpawnType.Human : SpawnType.Enemy); - break; - } + ThrowError(errorMsg); } - else + }, + null, + (Client client, Vector2 cursorPos, string[] args) => + { + string errorMsg; + SpawnCharacter(args, cursorPos, out errorMsg); + if (!string.IsNullOrWhiteSpace(errorMsg)) { - spawnPoint = WayPoint.GetRandom(args[0].ToLowerInvariant() == "human" ? SpawnType.Human : SpawnType.Enemy); - } - - if (string.IsNullOrWhiteSpace(args[0])) return; - - if (spawnPoint != null) spawnPosition = spawnPoint.WorldPosition; - - if (args[0].ToLowerInvariant() == "human") - { - spawnedCharacter = Character.Create(Character.HumanConfigFile, spawnPosition); - -#if CLIENT - if (GameMain.GameSession != null) - { - SinglePlayerCampaign mode = GameMain.GameSession.GameMode as SinglePlayerCampaign; - if (mode != null) - { - Character.Controlled = spawnedCharacter; - GameMain.GameSession.CrewManager.AddCharacter(Character.Controlled); - GameMain.GameSession.CrewManager.SelectCharacter(null, Character.Controlled); - } - } -#endif - } - else - { - List characterFiles = GameMain.Config.SelectedContentPackage.GetFilesOfType(ContentType.Character); - - foreach (string characterFile in characterFiles) - { - if (Path.GetFileNameWithoutExtension(characterFile).ToLowerInvariant() == args[0].ToLowerInvariant()) - { - Character.Create(characterFile, spawnPosition); - return; - } - } - - ThrowError("No character matching the name \"" + args[0] + "\" found in the selected content package."); - - //attempt to open the config from the default path (the file may still be present even if it isn't included in the content package) - string configPath = "Content/Characters/" - + args[0].First().ToString().ToUpper() + args[0].Substring(1) - + "/" + args[0].ToLower() + ".xml"; - Character.Create(configPath, spawnPosition); + ThrowError(errorMsg); } })); - commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory]: Spawn an item at the position of the cursor, in the inventory of the controlled character or at a random spawnpoint if the last parameter is omitted.", (string[] args) => + commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory]: Spawn an item at the position of the cursor, in the inventory of the controlled character or at a random spawnpoint if the last parameter is omitted.", + (string[] args) => { - if (args.Length < 1) return; - - Vector2? spawnPos = null; - Inventory spawnInventory = null; - - int extraParams = 0; - switch (args.Last()) + string errorMsg; + SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out errorMsg); + if (!string.IsNullOrWhiteSpace(errorMsg)) { - case "cursor": - extraParams = 1; - spawnPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - break; - case "inventory": - extraParams = 1; - spawnInventory = Character.Controlled == null ? null : Character.Controlled.Inventory; - break; - default: - extraParams = 0; - break; + ThrowError(errorMsg); } - - string itemName = string.Join(" ", args.Take(args.Length - extraParams)).ToLowerInvariant(); - - var itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + string errorMsg; + SpawnItem(args, cursorWorldPos, out errorMsg); + if (!string.IsNullOrWhiteSpace(errorMsg)) { - ThrowError("Item \"" + itemName + "\" not found!"); - return; - } - - if (spawnPos == null && spawnInventory == null) - { - var wp = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); - spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition; - } - - if (spawnPos != null) - { - Entity.Spawner.AddToSpawnQueue(itemPrefab, (Vector2)spawnPos); - - } - else if (spawnInventory != null) - { - Entity.Spawner.AddToSpawnQueue(itemPrefab, spawnInventory); + ThrowError(errorMsg); } })); @@ -279,12 +239,26 @@ namespace Barotrauma { HumanAIController.DisableCrewAI = true; NewMessage("Crew AI disabled", Color.White); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + HumanAIController.DisableCrewAI = true; + NewMessage("Crew AI disabled by \"" + client.Name + "\"", Color.White); + GameMain.Server.SendConsoleMessage("Crew AI disabled", client); })); commands.Add(new Command("enablecrewai", "enablecrewai: Enable the AI of the NPCs in the crew.", (string[] args) => { HumanAIController.DisableCrewAI = false; NewMessage("Crew AI enabled", Color.White); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + HumanAIController.DisableCrewAI = false; + NewMessage("Crew AI enabled by \"" + client.Name + "\"", Color.White); + GameMain.Server.SendConsoleMessage("Crew AI enabled", client); })); commands.Add(new Command("autorestart", "autorestart [true/false]: Enable or disable round auto-restart.", (string[] args) => @@ -310,7 +284,7 @@ namespace Barotrauma GameMain.NetLobbyScreen.LastUpdateID++; } NewMessage(GameMain.Server.AutoRestart ? "Automatic restart enabled." : "Automatic restart disabled.", Color.White); - })); + }, null, null)); commands.Add(new Command("autorestartinterval", "autorestartinterval [seconds]: Set how long the server waits between rounds before automatically starting a new one. If set to 0, autorestart is disabled.", (string[] args) => { @@ -338,7 +312,7 @@ namespace Barotrauma GameMain.NetLobbyScreen.LastUpdateID++; } } - })); + }, null, null)); commands.Add(new Command("autorestarttimer", "autorestarttimer [seconds]: Set the current autorestart countdown to the specified value.", (string[] args) => { @@ -367,10 +341,12 @@ namespace Barotrauma GameMain.NetLobbyScreen.LastUpdateID++; } } - })); + }, null, null)); commands.Add(new Command("giveperm", "giveperm [id]: Grants administrative permissions to the player with the specified client ID.", (string[] args) => { + //todo: allow client usage + if (GameMain.Server == null) return; if (args.Length < 1) { @@ -398,20 +374,79 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (perm.ToLower() == "all") { - permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign; + permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | + ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign | ClientPermissions.ConsoleCommands; } else { - if (!Enum.TryParse(perm, out permission)) + if (!Enum.TryParse(perm, true, out permission)) { - ThrowError("\"" + perm + "\" sn't a valid permission!"); + NewMessage(perm + " is not a valid permission!", Color.Red); return; } } - client.SetPermissions(client.Permissions | permission); + client.GivePermission(permission); GameMain.Server.UpdateClientPermissions(client); - DebugConsole.NewMessage("Granted "+perm+" permissions to "+client.Name+".",Color.White); + NewMessage("Granted " + perm + " permissions to " + client.Name + ".", Color.White); }); + }, + (string[] args) => + { +#if CLIENT + if (args.Length < 1) return; + + int id; + if (!int.TryParse(args[0], out id)) + { + ThrowError("\"" + id + "\" is not a valid client ID."); + return; + } + + NewMessage("Valid permissions are:", Color.White); + NewMessage(" - all", Color.White); + foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + { + NewMessage(" - " + permission.ToString(), Color.White); + } + ShowQuestionPrompt("Permission to grant to client #" + id + "?", (perm) => + { + GameMain.Client.SendConsoleCommand("giveperm " +id + " " + perm); + }); +#endif + }, + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (args.Length < 2) return; + + int id; + int.TryParse(args[0], out id); + var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); + if (client == null) + { + GameMain.Server.SendConsoleMessage("Client id \"" + id + "\" not found.", senderClient); + return; + } + + string perm = string.Join("", args.Skip(1)); + + ClientPermissions permission = ClientPermissions.None; + if (perm.ToLower() == "all") + { + permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | + ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign | ClientPermissions.ConsoleCommands; + } + else + { + if (!Enum.TryParse(perm, true, out permission)) + { + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + return; + } + } + client.GivePermission(permission); + GameMain.Server.UpdateClientPermissions(client); + GameMain.Server.SendConsoleMessage("Granted " + perm + " permissions to " + client.Name + ".", senderClient); + NewMessage(senderClient.Name + " granted " + perm + " permissions to " + client.Name + ".", Color.White); })); commands.Add(new Command("revokeperm", "revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", (string[] args) => @@ -443,20 +478,80 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (perm.ToLower() == "all") { - permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign; + permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | + ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign | ClientPermissions.ConsoleCommands; } else { - if (!Enum.TryParse(perm, out permission)) + if (!Enum.TryParse(perm, true, out permission)) { - ThrowError("\"" + perm + "\" isn't a valid permission!"); + NewMessage(perm + " is not a valid permission!", Color.Red); return; } } - client.SetPermissions(client.Permissions & ~permission); + client.RemovePermission(permission); GameMain.Server.UpdateClientPermissions(client); - DebugConsole.NewMessage("Revoked " + perm + " permissions from " + client.Name + ".", Color.White); + NewMessage("Revoked " + perm + " permissions from " + client.Name + ".", Color.White); }); + }, + (string[] args) => + { +#if CLIENT + if (args.Length < 1) return; + + int id; + if (!int.TryParse(args[0], out id)) + { + ThrowError("\"" + id + "\" is not a valid client ID."); + return; + } + + NewMessage("Valid permissions are:", Color.White); + NewMessage(" - all", Color.White); + foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + { + NewMessage(" - " + permission.ToString(), Color.White); + } + + ShowQuestionPrompt("Permission to revoke from client #" + id + "?", (perm) => + { + GameMain.Client.SendConsoleCommand("revokeperm " + id + " " + perm); + }); +#endif + }, + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (args.Length < 2) return; + + int id; + int.TryParse(args[0], out id); + var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); + if (client == null) + { + GameMain.Server.SendConsoleMessage("Client id \"" + id + "\" not found.", senderClient); + return; + } + + string perm = string.Join("", args.Skip(1)); + + ClientPermissions permission = ClientPermissions.None; + if (perm.ToLower() == "all") + { + permission = ClientPermissions.EndRound | ClientPermissions.Kick | ClientPermissions.Ban | + ClientPermissions.SelectSub | ClientPermissions.SelectMode | ClientPermissions.ManageCampaign | ClientPermissions.ConsoleCommands; + } + else + { + if (!Enum.TryParse(perm, true, out permission)) + { + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + return; + } + } + client.RemovePermission(permission); + GameMain.Server.UpdateClientPermissions(client); + GameMain.Server.SendConsoleMessage("Revoked " + perm + " permissions from " + client.Name + ".", senderClient); + NewMessage(senderClient.Name + " revoked " + perm + " permissions from " + client.Name + ".", Color.White); })); commands.Add(new Command("togglekarma", "togglekarma: Toggles the karma system.", (string[] args) => @@ -576,7 +671,7 @@ namespace Barotrauma } banDuration = parsedBanDuration; } - + var client = GameMain.Server.ConnectedClients.Find(c => c.Connection.RemoteEndPoint.Address.ToString() == args[0]); if (client == null) { @@ -611,6 +706,28 @@ namespace Barotrauma tpCharacter.Submarine = null; tpCharacter.AnimController.SetPosition(ConvertUnits.ToSimUnits(cam.ScreenToWorld(PlayerInput.MousePosition))); tpCharacter.AnimController.FindHull(cam.ScreenToWorld(PlayerInput.MousePosition), true); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character tpCharacter = null; + + if (args.Length == 0) + { + tpCharacter = client.Character; + } + else + { + tpCharacter = FindMatchingCharacter(args, false); + } + + if (tpCharacter == null) return; + + var cam = GameMain.GameScreen.Cam; + tpCharacter.AnimController.CurrentHull = null; + tpCharacter.Submarine = null; + tpCharacter.AnimController.SetPosition(ConvertUnits.ToSimUnits(cursorWorldPos)); + tpCharacter.AnimController.FindHull(cursorWorldPos, true); })); commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => @@ -619,17 +736,26 @@ namespace Barotrauma Submarine.MainSub.GodMode = !Submarine.MainSub.GodMode; NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (Submarine.MainSub == null) return; + + Submarine.MainSub.GodMode = !Submarine.MainSub.GodMode; + NewMessage((Submarine.MainSub.GodMode ? "Godmode turned on by \"" : "Godmode off by \"") + client.Name+"\"", Color.White); + GameMain.Server.SendConsoleMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", client); })); commands.Add(new Command("lockx", "lockx: Lock horizontal movement of the main submarine.", (string[] args) => { Submarine.LockX = !Submarine.LockX; - })); + }, null, null)); commands.Add(new Command("locky", "loxky: Lock vertical movement of the main submarine.", (string[] args) => { Submarine.LockY = !Submarine.LockY; - })); + }, null, null)); commands.Add(new Command("dumpids", "", (string[] args) => { @@ -656,6 +782,27 @@ namespace Barotrauma healedCharacter = FindMatchingCharacter(args); } + if (healedCharacter != null) + { + healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null); + healedCharacter.Oxygen = 100.0f; + healedCharacter.Bleeding = 0.0f; + healedCharacter.SetStun(0.0f, true); + } + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character healedCharacter = null; + if (args.Length == 0) + { + healedCharacter = client.Character; + } + else + { + healedCharacter = FindMatchingCharacter(args); + } + if (healedCharacter != null) { healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null); @@ -691,11 +838,44 @@ namespace Barotrauma break; } } + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character revivedCharacter = null; + if (args.Length == 0) + { + revivedCharacter = client.Character; + } + else + { + revivedCharacter = FindMatchingCharacter(args); + } + + if (revivedCharacter == null) return; + + revivedCharacter.Revive(false); + if (GameMain.Server != null) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Character != revivedCharacter) continue; + + //clients stop controlling the character when it dies, force control back + GameMain.Server.SetClientCharacter(c, revivedCharacter); + break; + } + } })); commands.Add(new Command("freeze", "", (string[] args) => { if (Character.Controlled != null) Character.Controlled.AnimController.Frozen = !Character.Controlled.AnimController.Frozen; + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (client.Character != null) client.Character.AnimController.Frozen = !client.Character.AnimController.Frozen; })); commands.Add(new Command("ragdoll", "ragdoll [character name]: Force-ragdoll the specified character. If the name parameter is omitted, the controlled character will be ragdolled.", (string[] args) => @@ -749,6 +929,17 @@ namespace Barotrauma if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); new Explosion(range, force, damage, structureDamage).Explode(explosionPos); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Vector2 explosionPos = cursorWorldPos; + float range = 500, force = 10, damage = 50, structureDamage = 10; + if (args.Length > 0) float.TryParse(args[0], out range); + if (args.Length > 1) float.TryParse(args[1], out force); + if (args.Length > 2) float.TryParse(args[2], out damage); + if (args.Length > 3) float.TryParse(args[3], out structureDamage); + new Explosion(range, force, damage, structureDamage).Explode(explosionPos); })); commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => @@ -757,7 +948,7 @@ namespace Barotrauma { it.Condition = it.Prefab.Health; } - })); + }, null, null)); commands.Add(new Command("fixhulls|fixwalls", "fixwalls/fixhulls: Fixes all walls.", (string[] args) => { @@ -768,7 +959,7 @@ namespace Barotrauma w.AddDamage(i, -100000.0f); } } - })); + }, null, null)); commands.Add(new Command("power", "power [temperature]: Immediately sets the temperature of the nuclear reactor to the specified value.", (string[] args) => { @@ -787,7 +978,7 @@ namespace Barotrauma { reactorItem.CreateServerEvent(reactor); } - })); + }, null, null)); commands.Add(new Command("oxygen|air", "oxygen/air: Replenishes the oxygen levels in every room to 100%.", (string[] args) => { @@ -795,7 +986,7 @@ namespace Barotrauma { hull.OxygenPercentage = 100.0f; } - })); + }, null, null)); commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { @@ -822,7 +1013,7 @@ namespace Barotrauma if (!(c.AIController is EnemyAIController)) continue; c.AddDamage(CauseOfDeath.Damage, c.MaxHealth * 2, null); } - })); + }, null, null)); commands.Add(new Command("netstats", "netstats: Toggles the visibility of the network statistics UI.", (string[] args) => { @@ -835,7 +1026,6 @@ namespace Barotrauma if (GameMain.Server == null) return; int separatorIndex = Array.IndexOf(args, ";"); - if (separatorIndex == -1 || args.Length < 3) { ThrowError("Invalid parameters. The command should be formatted as \"setclientcharacter [client] ; [character]\""); @@ -844,8 +1034,7 @@ namespace Barotrauma string[] argsLeft = args.Take(separatorIndex).ToArray(); string[] argsRight = args.Skip(separatorIndex + 1).ToArray(); - - string clientName = String.Join(" ", argsLeft); + string clientName = string.Join(" ", argsLeft); var client = GameMain.Server.ConnectedClients.Find(c => c.Name == clientName); if (client == null) @@ -853,6 +1042,29 @@ namespace Barotrauma ThrowError("Client \"" + clientName + "\" not found."); } + var character = FindMatchingCharacter(argsRight, false); + GameMain.Server.SetClientCharacter(client, character); + }, + null, + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + int separatorIndex = Array.IndexOf(args, ";"); + if (separatorIndex == -1 || args.Length < 3) + { + GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] ; [character]\"", senderClient); + return; + } + + string[] argsLeft = args.Take(separatorIndex).ToArray(); + string[] argsRight = args.Skip(separatorIndex + 1).ToArray(); + string clientName = string.Join(" ", argsLeft); + + var client = GameMain.Server.ConnectedClients.Find(c => c.Name == clientName); + if (client == null) + { + GameMain.Server.SendConsoleMessage("Client \"" + clientName + "\" not found.", senderClient); + } + var character = FindMatchingCharacter(argsRight, false); GameMain.Server.SetClientCharacter(client, character); })); @@ -913,6 +1125,69 @@ namespace Barotrauma campaign.Map.SelectLocation(location); NewMessage(location.Name + " selected.", Color.White); } + }, + (string[] args) => + { +#if CLIENT + var campaign = GameMain.GameSession?.GameMode as CampaignMode; + if (campaign == null) + { + ThrowError("No campaign active!"); + return; + } + + if (args.Length == 0) + { + int i = 0; + foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) + { + NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); + i++; + } + ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => + { + int destinationIndex = -1; + if (!int.TryParse(selectedDestination, out destinationIndex)) return; + if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count) + { + NewMessage("Index out of bounds!", Color.Red); + return; + } + GameMain.Client.SendConsoleCommand("campaigndestination " + destinationIndex); + }); + } + else + { + int destinationIndex = -1; + if (!int.TryParse(args[0], out destinationIndex)) return; + if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count) + { + NewMessage("Index out of bounds!", Color.Red); + return; + } + GameMain.Client.SendConsoleCommand("campaigndestination " + destinationIndex); + } +#endif + }, + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + var campaign = GameMain.GameSession?.GameMode as CampaignMode; + if (campaign == null) + { + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + return; + } + + int destinationIndex = -1; + if (args.Length < 1 || !int.TryParse(args[0], out destinationIndex)) return; + if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count) + { + GameMain.Server.SendConsoleMessage("Index out of bounds!", senderClient); + return; + } + Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); + campaign.Map.SelectLocation(location); + GameMain.Server.SendConsoleMessage(location.Name + " selected.", senderClient); })); #if DEBUG @@ -947,7 +1222,7 @@ namespace Barotrauma { GameMain.Server.CreateEntityEvent(wall); } - })); + }, null, null)); #endif InitProjectSpecific(); @@ -1035,7 +1310,7 @@ namespace Barotrauma return Messages[selectedIndex].Text; } - public static void ExecuteCommand(string command, GameMain game) + public static void ExecuteCommand(string command) { if (activeQuestionCallback != null) { @@ -1059,11 +1334,33 @@ namespace Barotrauma NewMessage(command, Color.White); } -#if !DEBUG && CLIENT - if (GameMain.Client != null && !IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) +#if CLIENT + if (GameMain.Client != null) { - ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!"); - return; + if (GameMain.Client.HasConsoleCommandPermission(splitCommand[0].ToLowerInvariant())) + { + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + + //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side + if (matchingCommand == null || matchingCommand.RelayToServer) + { + GameMain.Client.SendConsoleCommand(command); + } + else + { + matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray()); + } + + NewMessage("Server command: " + command, Color.White); + return; + } +#if !DEBUG + if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) + { + ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!"); + return; + } +#endif } #endif @@ -1080,10 +1377,47 @@ namespace Barotrauma if (!commandFound) { - ThrowError("Command \""+splitCommand[0]+"\" not found."); + ThrowError("Command \"" + splitCommand[0] + "\" not found."); } } - + + public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command) + { + if (GameMain.Server == null) return; + if (string.IsNullOrWhiteSpace(command)) return; + if (!client.HasPermission(ClientPermissions.ConsoleCommands)) + { + GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client); + GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); + return; + } + + string[] splitCommand = SplitCommand(command); + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand)) + { + GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client); + GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); + return; + } + else if (matchingCommand == null) + { + GameMain.Server.SendConsoleMessage("Command \"" + splitCommand[0] + "\" not found.", client); + return; + } + + try + { + matchingCommand.ServerExecuteOnClientRequest(client, cursorWorldPos, splitCommand.Skip(1).ToArray()); + GameServer.Log("Console command \"" + command + "\" executed by " + client.Name + ".", ServerLog.MessageType.ConsoleUsage); + } + catch (Exception e) + { + ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + client.Name + "\" failed.", e); + } + } + + private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false) { if (args.Length == 0) return null; @@ -1131,6 +1465,152 @@ namespace Barotrauma return null; } + private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg) + { + errorMsg = ""; + if (args.Length == 0) return; + + Character spawnedCharacter = null; + + Vector2 spawnPosition = Vector2.Zero; + WayPoint spawnPoint = null; + + if (args.Length > 1) + { + switch (args[1].ToLowerInvariant()) + { + case "inside": + spawnPoint = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); + break; + case "outside": + spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); + break; + case "near": + case "close": + float closestDist = -1.0f; + foreach (WayPoint wp in WayPoint.WayPointList) + { + if (wp.Submarine != null) continue; + + //don't spawn inside hulls + if (Hull.FindHull(wp.WorldPosition, null) != null) continue; + + float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter); + + if (closestDist < 0.0f || dist < closestDist) + { + spawnPoint = wp; + closestDist = dist; + } + } + break; + case "cursor": + spawnPosition = cursorWorldPos; + break; + default: + spawnPoint = WayPoint.GetRandom(args[0].ToLowerInvariant() == "human" ? SpawnType.Human : SpawnType.Enemy); + break; + } + } + else + { + spawnPoint = WayPoint.GetRandom(args[0].ToLowerInvariant() == "human" ? SpawnType.Human : SpawnType.Enemy); + } + + if (string.IsNullOrWhiteSpace(args[0])) return; + + if (spawnPoint != null) spawnPosition = spawnPoint.WorldPosition; + + if (args[0].ToLowerInvariant() == "human") + { + spawnedCharacter = Character.Create(Character.HumanConfigFile, spawnPosition); + +#if CLIENT + if (GameMain.GameSession != null) + { + SinglePlayerCampaign mode = GameMain.GameSession.GameMode as SinglePlayerCampaign; + if (mode != null) + { + Character.Controlled = spawnedCharacter; + GameMain.GameSession.CrewManager.AddCharacter(Character.Controlled); + GameMain.GameSession.CrewManager.SelectCharacter(null, Character.Controlled); + } + } +#endif + } + else + { + List characterFiles = GameMain.Config.SelectedContentPackage.GetFilesOfType(ContentType.Character); + + foreach (string characterFile in characterFiles) + { + if (Path.GetFileNameWithoutExtension(characterFile).ToLowerInvariant() == args[0].ToLowerInvariant()) + { + Character.Create(characterFile, spawnPosition); + return; + } + } + + errorMsg = "No character matching the name \"" + args[0] + "\" found in the selected content package."; + + //attempt to open the config from the default path (the file may still be present even if it isn't included in the content package) + string configPath = "Content/Characters/" + + args[0].First().ToString().ToUpper() + args[0].Substring(1) + + "/" + args[0].ToLower() + ".xml"; + Character.Create(configPath, spawnPosition); + } + } + + private static void SpawnItem(string[] args, Vector2 cursorPos, out string errorMsg) + { + errorMsg = ""; + if (args.Length < 1) return; + + Vector2? spawnPos = null; + Inventory spawnInventory = null; + + int extraParams = 0; + switch (args.Last()) + { + case "cursor": + extraParams = 1; + spawnPos = cursorPos; + break; + case "inventory": + extraParams = 1; + spawnInventory = Character.Controlled == null ? null : Character.Controlled.Inventory; + break; + default: + extraParams = 0; + break; + } + + string itemName = string.Join(" ", args.Take(args.Length - extraParams)).ToLowerInvariant(); + + var itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (itemPrefab == null) + { + errorMsg = "Item \"" + itemName + "\" not found!"; + return; + } + + if (spawnPos == null && spawnInventory == null) + { + var wp = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); + spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition; + } + + if (spawnPos != null) + { + Entity.Spawner.AddToSpawnQueue(itemPrefab, (Vector2)spawnPos); + + } + else if (spawnInventory != null) + { + Entity.Spawner.AddToSpawnQueue(itemPrefab, spawnInventory); + } + } + public static void NewMessage(string msg, Color color) { if (string.IsNullOrEmpty((msg))) return; @@ -1158,14 +1638,15 @@ namespace Barotrauma public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered) { - NewMessage(" >>" + question, Color.Cyan); - activeQuestionCallback += onAnswered; + #if CLIENT - if (listBox != null && listBox.children.Count > 0) - { - activeQuestionText = listBox.children[listBox.children.Count - 1]; - } + activeQuestionText = new GUITextBlock(new Rectangle(0, 0, listBox.Rect.Width, 30), " >>" + question, "", Alignment.TopLeft, Alignment.Left, null, true, GUI.SmallFont); + activeQuestionText.CanBeFocused = false; + activeQuestionText.TextColor = Color.Cyan; +#else + NewMessage(" >>" + question, Color.Cyan); #endif + activeQuestionCallback += onAnswered; } private static bool TryParseTimeSpan(string s, out TimeSpan timeSpan) @@ -1217,6 +1698,12 @@ namespace Barotrauma return true; } + public static Command FindCommand(string commandName) + { + commandName = commandName.ToLowerInvariant(); + return commands.Find(c => c.names.Any(n => n.ToLowerInvariant() == commandName)); + } + public static void Log(string message) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs index b6bc208fc..74ae7f130 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs @@ -103,11 +103,7 @@ namespace Barotrauma.Items.Components { if (character == null) return false; if (!character.IsKeyDown(InputType.Aim)) return false; - - //if (DoesUseFail(Character)) return false; - - //targetPosition = targetPosition.X, -targetPosition.Y); - + float degreeOfSuccess = DegreeOfSuccess(character)/100.0f; if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) @@ -241,7 +237,7 @@ namespace Barotrauma.Items.Components { Gap leak = objective.OperateTarget as Gap; if (leak == null) return true; - + float dist = Vector2.Distance(leak.WorldPosition, item.WorldPosition); //too far away -> consider this done and hope the AI is smart enough to move closer diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index 29d5c60e4..5af91073e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -258,9 +258,9 @@ namespace Barotrauma.Items.Components if (batteryToLoad == null) return true; - if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed*0.4f) + if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f) { - objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, "")); + objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, "", false)); return false; } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 6e2622276..beb737fb9 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -678,14 +678,13 @@ namespace Barotrauma if (!MathUtils.IsValid(damage)) return; - float damageDiff = damage - sections[sectionIndex].damage; + if (GameMain.Server != null && damage != sections[sectionIndex].damage) { GameMain.Server.CreateEntityEvent(this); } - AdjustKarma(attacker, damageDiff); if (damage < prefab.Health*0.5f) { if (sections[sectionIndex].gap != null) @@ -717,10 +716,14 @@ namespace Barotrauma sections[sectionIndex].gap.Open = (damage / prefab.Health - 0.5f) * 2.0f; } - + + float damageDiff = damage - sections[sectionIndex].damage; bool hadHole = SectionBodyDisabled(sectionIndex); sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, prefab.Health); + if (sections[sectionIndex].damage < prefab.Health) //otherwise it's possible to infinitely gain karma by welding fixed things + AdjustKarma(attacker, damageDiff); + bool hasHole = SectionBodyDisabled(sectionIndex); if (hadHole == hasHole) return; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs index e69dbf80f..b2f1d18d1 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Networking { enum ChatMessageType { - Default, Error, Dead, Server, Radio, Private, MessageBox + Default, Error, Dead, Server, Radio, Private, Console, MessageBox } partial class ChatMessage @@ -25,7 +25,8 @@ namespace Barotrauma.Networking new Color(63, 72, 204), //dead new Color(157, 225, 160), //server new Color(238, 208, 0), //radio - new Color(64, 240, 89) //private + new Color(64, 240, 89), //private + new Color(255, 255, 255) //console }; public readonly string Text; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs index 268c701f8..f380611f0 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs @@ -1,29 +1,10 @@ using Lidgren.Network; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; namespace Barotrauma.Networking { - [Flags] - enum ClientPermissions - { - None = 0, - [Description("End round")] - EndRound = 1, - [Description("Kick")] - Kick = 2, - [Description("Ban")] - Ban = 4, - [Description("Select submarine")] - SelectSub = 8, - [Description("Select game mode")] - SelectMode = 16, - [Description("Manage campaign")] - ManageCampaign = 32 - } - class Client { public string Name; @@ -96,6 +77,11 @@ namespace Barotrauma.Networking public float DeleteDisconnectedTimer; public ClientPermissions Permissions = ClientPermissions.None; + public List PermittedConsoleCommands + { + get; + private set; + } public bool SpectateOnly; @@ -130,6 +116,7 @@ namespace Barotrauma.Networking this.Name = name; this.ID = ID; + PermittedConsoleCommands = new List(); kickVoters = new List(); votes = new object[Enum.GetNames(typeof(VoteType)).Length]; @@ -171,9 +158,10 @@ namespace Barotrauma.Networking return rName; } - public void SetPermissions(ClientPermissions permissions) + public void SetPermissions(ClientPermissions permissions, List permittedConsoleCommands) { this.Permissions = permissions; + this.PermittedConsoleCommands = new List(permittedConsoleCommands); } public void GivePermission(ClientPermissions permission) diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs new file mode 100644 index 000000000..262503795 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Xml.Linq; + +namespace Barotrauma.Networking +{ + [Flags] + enum ClientPermissions + { + None = 0, + [Description("End round")] + EndRound = 1, + [Description("Kick")] + Kick = 2, + [Description("Ban")] + Ban = 4, + [Description("Select submarine")] + SelectSub = 8, + [Description("Select game mode")] + SelectMode = 16, + [Description("Manage campaign")] + ManageCampaign = 32, + [Description("Console commands")] + ConsoleCommands = 64 + } + + class PermissionPreset + { + public static List List = new List(); + + public readonly string Name; + public readonly string Description; + public readonly ClientPermissions Permissions; + public readonly List PermittedCommands; + + public PermissionPreset(XElement element) + { + Name = element.GetAttributeString("name", ""); + Description = element.GetAttributeString("description", ""); + + string permissionsStr = element.GetAttributeString("permissions", ""); + if (!Enum.TryParse(permissionsStr, out Permissions)) + { + DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + permissionsStr + " is not a valid permission!"); + } + + PermittedCommands = new List(); + if (Permissions.HasFlag(ClientPermissions.ConsoleCommands)) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "command") continue; + string commandName = subElement.GetAttributeString("name", ""); + + DebugConsole.Command command = DebugConsole.FindCommand(commandName); + if (command == null) + { + DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + commandName + "\" is not a valid console command."); + continue; + } + + PermittedCommands.Add(command); + } + } + } + + public static void LoadAll(string file) + { + if (!File.Exists(file)) return; + + XDocument doc = XMLExtensions.TryLoadXml(file); + if (doc == null || doc.Root == null) return; + + foreach (XElement element in doc.Root.Elements()) + { + List.Add(new PermissionPreset(element)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs index b40f789c1..9094b43bc 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs @@ -125,8 +125,9 @@ namespace Barotrauma.Networking banList = new BanList(); LoadSettings(); + PermissionPreset.LoadAll(PermissionPresetFile); LoadClientPermissions(); - + CoroutineManager.StartCoroutine(StartServer(isPublic)); } @@ -795,6 +796,11 @@ namespace Barotrauma.Networking campaign.ServerRead(inc, sender); } break; + case ClientPermissions.ConsoleCommands: + string consoleCommand = inc.ReadString(); + Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); + DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand); + break; } inc.ReadPadBits(); @@ -853,7 +859,7 @@ namespace Barotrauma.Networking outmsg.Write(GameStarted); outmsg.Write(AllowSpectating); - outmsg.Write((byte)c.Permissions); + WritePermissions(outmsg, c); } private void ClientWriteIngame(Client c) @@ -1636,6 +1642,18 @@ namespace Barotrauma.Networking } } + public void SendChatMessage(string txt, Client recipient) + { + ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Server, null); + SendChatMessage(msg, recipient); + } + + public void SendConsoleMessage(string txt, Client recipient) + { + ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, null); + SendChatMessage(msg, recipient); + } + public void SendChatMessage(ChatMessage msg, Client recipient) { msg.NetStateID = recipient.ChatMsgQueue.Count > 0 ? @@ -1936,16 +1954,34 @@ namespace Barotrauma.Networking clientPermissions.Add(new SavedClientPermission( client.Name, client.Connection.RemoteEndPoint.Address.ToString(), - client.Permissions)); + client.Permissions, + client.PermittedConsoleCommands)); } var msg = server.CreateMessage(); msg.Write((byte)ServerPacketHeader.PERMISSIONS); - msg.Write((byte)client.Permissions); + WritePermissions(msg, client); + server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered); SaveClientPermissions(); } + + private void WritePermissions(NetBuffer msg, Client client) + { + msg.Write((byte)client.Permissions); + if (client.Permissions.HasFlag(ClientPermissions.ConsoleCommands)) + { + msg.Write((UInt16)client.PermittedConsoleCommands.Sum(c => c.names.Length)); + foreach (DebugConsole.Command command in client.PermittedConsoleCommands) + { + foreach (string commandName in command.names) + { + msg.Write(commandName); + } + } + } + } public void SetClientCharacter(Client client, Character newCharacter) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/GameServerLogin.cs b/Barotrauma/BarotraumaShared/Source/Networking/GameServerLogin.cs index 3208348b0..c40d8f9d3 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/GameServerLogin.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/GameServerLogin.cs @@ -179,7 +179,7 @@ namespace Barotrauma.Networking DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name taken by the server)", Color.Red); return; } - Client nameTaken = ConnectedClients.Find(c => c.Name.ToLower() == clName.ToLower()); + Client nameTaken = ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), clName.ToLower())); if (nameTaken != null) { if (nameTaken.Connection.RemoteEndPoint.Address.ToString() == inc.SenderEndPoint.Address.ToString()) @@ -218,11 +218,11 @@ namespace Barotrauma.Networking var savedPermissions = clientPermissions.Find(cp => cp.IP == newClient.Connection.RemoteEndPoint.Address.ToString()); if (savedPermissions != null) { - newClient.SetPermissions(savedPermissions.Permissions); + newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands); } else { - newClient.SetPermissions(ClientPermissions.None); + newClient.SetPermissions(ClientPermissions.None, new List()); } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs index ade900b05..cc07cf19e 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs @@ -25,20 +25,23 @@ namespace Barotrauma.Networking { public readonly string IP; public readonly string Name; + public List PermittedCommands; public ClientPermissions Permissions; - public SavedClientPermission(string name, string ip, ClientPermissions permissions) + public SavedClientPermission(string name, string ip, ClientPermissions permissions, List permittedCommands) { this.Name = name; this.IP = ip; this.Permissions = permissions; + this.PermittedCommands = permittedCommands; } } public const string SettingsFile = "serversettings.xml"; - public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.txt"; + public static readonly string PermissionPresetFile = "Data" + Path.DirectorySeparatorChar + "permissionpresets.xml"; + public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public Dictionary SerializableProperties { @@ -130,6 +133,13 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, true)] + public bool AllowRagdollButton + { + get; + private set; + } + [Serialize(true, true)] public bool AllowFileTransfers { @@ -210,6 +220,20 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, true)] + public bool TraitorUseRatio + { + get; + private set; + } + + [Serialize(0.2f, true)] + public float TraitorRatio + { + get; + private set; + } + [Serialize(false,true)] public bool KarmaEnabled { @@ -323,12 +347,67 @@ namespace Barotrauma.Networking public void LoadClientPermissions() { - if (!File.Exists(ClientPermissionsFile)) return; - + clientPermissions.Clear(); + + if (File.Exists("Data/clientpermissions.txt") && !File.Exists(ClientPermissionsFile)) + { + LoadClientPermissionsOld("Data/clientpermissions.txt"); + return; + } + + XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile); + foreach (XElement clientElement in doc.Root.Elements()) + { + string clientName = clientElement.GetAttributeString("name", ""); + string clientIP = clientElement.GetAttributeString("ip", ""); + if (string.IsNullOrWhiteSpace(clientName) || string.IsNullOrWhiteSpace(clientIP)) + { + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name and an IP address."); + continue; + } + + string permissionsStr = clientElement.GetAttributeString("permissions", ""); + ClientPermissions permissions; + if (!Enum.TryParse(permissionsStr, out permissions)) + { + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + permissionsStr + "\" is not a valid client permission."); + continue; + } + + List permittedCommands = new List(); + if (permissions.HasFlag(ClientPermissions.ConsoleCommands)) + { + foreach (XElement commandElement in clientElement.Elements()) + { + if (commandElement.Name.ToString().ToLowerInvariant() != "command") continue; + + string commandName = commandElement.GetAttributeString("name", ""); + DebugConsole.Command command = DebugConsole.FindCommand(commandName); + if (command == null) + { + DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + commandName + "\" is not a valid console command."); + continue; + } + + permittedCommands.Add(command); + } + } + + clientPermissions.Add(new SavedClientPermission(clientName, clientIP, permissions, permittedCommands)); + } + } + + /// + /// Method for loading old .txt client permission files to provide backwards compatibility + /// + private void LoadClientPermissionsOld(string file) + { + if (!File.Exists(file)) return; + string[] lines; try { - lines = File.ReadAllLines(ClientPermissionsFile); + lines = File.ReadAllLines(file); } catch (Exception e) { @@ -337,36 +416,63 @@ namespace Barotrauma.Networking } clientPermissions.Clear(); + foreach (string line in lines) { string[] separatedLine = line.Split('|'); if (separatedLine.Length < 3) continue; - string name = String.Join("|", separatedLine.Take(separatedLine.Length - 2)); + string name = string.Join("|", separatedLine.Take(separatedLine.Length - 2)); string ip = separatedLine[separatedLine.Length - 2]; ClientPermissions permissions = ClientPermissions.None; - if (Enum.TryParse(separatedLine.Last(), out permissions)) + if (Enum.TryParse(separatedLine.Last(), out permissions)) { - clientPermissions.Add(new SavedClientPermission(name, ip, permissions)); + clientPermissions.Add(new SavedClientPermission(name, ip, permissions, new List())); } } } public void SaveClientPermissions() { - GameServer.Log("Saving client permissions", ServerLog.MessageType.ServerMessage); + //delete old client permission file + if (File.Exists("Data/clientpermissions.txt")) + { + File.Delete("Data/clientpermissions.txt"); + } - List lines = new List(); + Log("Saving client permissions", ServerLog.MessageType.ServerMessage); + + XDocument doc = new XDocument(new XElement("ClientPermissions")); foreach (SavedClientPermission clientPermission in clientPermissions) { - lines.Add(clientPermission.Name + "|" + clientPermission.IP+"|"+clientPermission.Permissions.ToString()); + XElement clientElement = new XElement("Client", + new XAttribute("name", clientPermission.Name), + new XAttribute("ip", clientPermission.IP), + new XAttribute("permissions", clientPermission.Permissions.ToString())); + + if (clientPermission.Permissions.HasFlag(ClientPermissions.ConsoleCommands)) + { + foreach (DebugConsole.Command command in clientPermission.PermittedCommands) + { + clientElement.Add(new XElement("command", new XAttribute("name", command.names[0]))); + } + } + + doc.Root.Add(clientElement); } try { - File.WriteAllLines(ClientPermissionsFile, lines); + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.NewLineOnAttributes = true; + + using (var writer = XmlWriter.Create(ClientPermissionsFile, settings)) + { + doc.Save(writer); + } } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs index cea50e2e8..74954b977 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs @@ -28,18 +28,20 @@ namespace Barotrauma.Networking Attack, Spawning, ServerMessage, + ConsoleUsage, Error } private readonly Color[] messageColor = { - Color.LightBlue, - new Color(255, 142, 0), - new Color(238, 208, 0), - new Color(204, 74, 78), - new Color(163, 73, 164), - new Color(157, 225, 160), - Color.Red + Color.LightBlue, //Chat + new Color(255, 142, 0), //ItemInteraction + new Color(238, 208, 0), //Inventory + new Color(204, 74, 78), //Attack + new Color(163, 73, 164), //Spawning + new Color(157, 225, 160), //ServerMessage + new Color(0, 162, 232), //ConsoleUsage + Color.Red //Error }; private readonly string[] messageTypeName = @@ -50,6 +52,7 @@ namespace Barotrauma.Networking "Attack & death", "Spawning", "Server message", + "Console usage", "Error" }; diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 1eb8f2d21..18abd505a 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -15,11 +15,15 @@ allowspectating="True" endroundatlevelend="True" saveserverlogs="True" + allowragdollbutton="True" allowfiletransfers="True" allowrespawn="True" allowvotekick="True" endvoterequiredratio="0.6" kickvoterequiredratio="0.6" + traitoruseratio="True" + traitorratio="0.2" + karmaenabled="False" SubSelection="Manual" ModeSelection="Manual" TraitorsEnabled="No"