diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 0fea47a53..e34cbb98f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -525,6 +525,7 @@ namespace Barotrauma ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); string originalName = inc.ReadString(); + bool renamingEnabled = inc.ReadBoolean(); int tagCount = inc.ReadByte(); HashSet tagSet = new HashSet(); for (int i = 0; i < tagCount; i++) @@ -538,7 +539,8 @@ namespace Barotrauma Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); - + + Identifier npcId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier(); @@ -571,7 +573,8 @@ namespace Barotrauma CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId) { ID = infoID, - MinReputationToHire = (factionId, minReputationToHire) + MinReputationToHire = (factionId, minReputationToHire), + RenamingEnabled = renamingEnabled }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 3ade24b3d..ee0137f93 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -357,6 +357,7 @@ namespace Barotrauma case EventType.Control: bool myCharacter = msg.ReadBoolean(); byte ownerID = msg.ReadByte(); + bool renamingEnabled = msg.ReadBoolean(); ResetNetState(); if (myCharacter) { @@ -385,6 +386,10 @@ namespace Barotrauma } IsRemotePlayer = ownerID > 0; } + if (info != null) + { + info.RenamingEnabled = renamingEnabled; + } break; case EventType.Status: ReadStatus(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs index 3042c060f..f1cc3b397 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs @@ -427,7 +427,7 @@ internal class DeathPrompt var botList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), content.RectTransform)); foreach (CharacterInfo c in GetAvailableBots()) { - var characterFrame = campaign.CampaignUI?.CrewManagement.CreateCharacterFrame(c, botList, hideSalary: true); + var characterFrame = campaign.CampaignUI?.HRManagerUI.CreateCharacterFrame(c, botList, hideSalary: true); if (characterFrame != null) { characterFrame.UserData = c; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs similarity index 93% rename from Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs rename to Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs index 4749ace03..b5e0531d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs @@ -8,13 +8,16 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { - class CrewManagement + /// + /// The "HR manager" UI, which is used to hire/fire characters and rename crewmates. + /// + class HRManagerUI { private CampaignMode campaign => campaignUI.Campaign; private readonly CampaignUI campaignUI; private readonly GUIComponent parentComponent; - private GUILayoutGroup pendingAndCrewGroup; + private GUIComponent pendingAndCrewPanel; private GUIListBox hireableList, pendingList, crewList; private GUIFrame characterPreviewFrame; private GUIDropDown sortingDropDown; @@ -25,14 +28,21 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; - - // Is the player hiring a new character for themselves instead of bots for the crew? - // The window can only be used for one of these purposes at the same time. - private static bool HiringNewCharacter => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } && - GameMain.Client?.CharacterInfo is { PermanentlyDead: true }; - private static bool HasPermissionToHire => CampaignMode.AllowedToManageCampaign( - HiringNewCharacter ? ClientPermissions.ManageMoney : ClientPermissions.ManageHires); + + private bool wasReplacingPermanentlyDeadCharacter; + /// + /// Is the player hiring a new character for themselves instead of bots for the crew? + /// The window can only be used for one of these purposes at the same time. + /// + private static bool ReplacingPermanentlyDeadCharacter => + GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } && + GameMain.Client?.CharacterInfo is { PermanentlyDead: true }; + + private bool hadPermissionToHire; + private static bool HasPermissionToHire => ReplacingPermanentlyDeadCharacter ? + GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage <= 0 || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMoney) || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires) : + CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires); private Point resolutionWhenCreated; @@ -48,7 +58,7 @@ namespace Barotrauma SkillDesc } - public CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent) + public HRManagerUI(CampaignUI campaignUI, GUIComponent parentComponent) { this.campaignUI = campaignUI; this.parentComponent = parentComponent; @@ -61,14 +71,19 @@ namespace Barotrauma (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); + + hadPermissionToHire = HasPermissionToHire; + wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter; } - public void RefreshPermissions() + public void RefreshUI() { RefreshCrewFrames(hireableList); RefreshCrewFrames(crewList); RefreshCrewFrames(pendingList); if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; } + hadPermissionToHire = HasPermissionToHire; + wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter; } private void RefreshCrewFrames(GUIListBox listBox) @@ -80,8 +95,11 @@ namespace Barotrauma if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; - bool enougMoneyToHire = !HiringNewCharacter || campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); - buyButton.Enabled = HasPermissionToHire && EnoughReputationToHire(characterInfo) && enougMoneyToHire; + buyButton.Enabled = + //"normal buying" is disabled when replacing a dead character + !ReplacingPermanentlyDeadCharacter && + HasPermissionToHire && + EnoughReputationToHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo); foreach (GUITextBlock text in child.GetAllChildren()) { text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); @@ -182,11 +200,13 @@ namespace Barotrauma playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f)); - pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, - parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) - { - MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) - }).RectTransform)); + pendingAndCrewPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) + { + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + }); + + var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, + parent: pendingAndCrewPanel.RectTransform)); float height = 0.05f; new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont); @@ -456,7 +476,7 @@ namespace Barotrauma if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)), + TextManager.FormatCurrency(ReplacingPermanentlyDeadCharacter ? campaign.NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo)), textAlignment: Alignment.Center) { CanBeFocused = false @@ -476,12 +496,12 @@ namespace Barotrauma ToolTip = TextManager.Get("hirebutton"), ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = CanHire(characterInfo) && !HiringNewCharacter, + Enabled = CanHire(characterInfo) && !ReplacingPermanentlyDeadCharacter, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => { - if (HiringNewCharacter) + if (ReplacingPermanentlyDeadCharacter) { return; } @@ -500,9 +520,9 @@ namespace Barotrauma } }; - if (HiringNewCharacter) + if (ReplacingPermanentlyDeadCharacter) { - bool canHire = CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); + bool canHire = CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo); var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton") { ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"), @@ -516,7 +536,7 @@ namespace Barotrauma return false; } Client client = gameClient.ConnectedClients.FirstOrDefault(c => c.SessionId == gameClient.SessionId); - if (!campaign.TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) + if (!campaign.TryPurchase(client, campaign.NewCharacterCost(characterInfo))) { return false; } @@ -528,7 +548,7 @@ namespace Barotrauma }; takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) => { - bool canHireCurrently = HiringNewCharacter && CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); + bool canHireCurrently = ReplacingPermanentlyDeadCharacter && CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo); btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled"); btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true }; btn.Enabled = canHireCurrently; @@ -931,9 +951,15 @@ namespace Barotrauma { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } - + // When showing this window to someone hiring a new character, the right side panels aren't needed - pendingAndCrewGroup.Visible = !HiringNewCharacter; + pendingAndCrewPanel.Visible = !ReplacingPermanentlyDeadCharacter; + + if (hadPermissionToHire != HasPermissionToHire || + wasReplacingPermanentlyDeadCharacter != ReplacingPermanentlyDeadCharacter) + { + RefreshUI(); + } if (needsHireableRefresh) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index f914358fa..1b6fad62f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -71,7 +71,9 @@ namespace Barotrauma private HashSet selectedTalents = new HashSet(); private readonly Queue showCaseClosureQueue = new(); - + + private GUITextBlock? nameBlock; + private GUIButton? renameButton; private GUIListBox? skillListBox; private GUITextBlock? talentPointText; private GUIProgressBar? experienceBar; @@ -134,7 +136,6 @@ namespace Barotrauma GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter)); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); - // TODO: What is CampaignCharacterDiscarded and can it be relevant in permadeath mode? if (!GameMain.NetLobbyScreen.PermadeathMode) { GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), @@ -174,6 +175,25 @@ namespace Barotrauma } }; } + else if (characterInfo != null) + { + renameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), + text: TextManager.Get("button.RenameCharacter"), style: "GUIButtonSmall") + { + Enabled = characterInfo.RenamingEnabled, + ToolTip = TextManager.Get("permadeath.rename.description"), + IgnoreLayoutGroups = false, + TextBlock = + { + AutoScaleHorizontal = true + }, + OnClicked = (_, _) => + { + CreateRenamePopup(); + return true; + } + }; + } GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? @@ -189,6 +209,57 @@ namespace Barotrauma }; } + private void CreateRenamePopup() + { + GUIMessageBox renamePopup = new( + TextManager.Get("button.RenameCharacter"), TextManager.Get("permadeath.rename.description"), + new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel") }, minSize: new Point(0, GUI.IntScale(230))); + GUITextBox newNameBox = new(new(Vector2.One, renamePopup.Content.RectTransform), "") + { + OnEnterPressed = (textBox, text) => + { + textBox.Text = text.Trim(); + return true; + } + }; + renamePopup.Buttons[0].OnClicked += (_, _) => + { + if (newNameBox.Text?.Trim() is string newName && newName != "") + { + if (characterInfo != null) + { + if (newNameBox.Text == characterInfo.Name) + { + renamePopup.Close(); + return true; + } + if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement) + { + crewManagement.RenameCharacter(characterInfo, newName); + if (nameBlock != null) + { + nameBlock.Text = newName; + } + if (renameButton != null) + { + renameButton.Enabled = false; + } + renamePopup.Close(); + } + return true; + } + DebugConsole.ThrowError("Tried to rename character, but CharacterInfo completely missing!"); + return true; + } + else + { + newNameBox.Flash(); + return false; + } + }; + renamePopup.Buttons[1].OnClicked += renamePopup.Close; + } + private void CreateStatPanel(GUIComponent parent, CharacterInfo info) { Job job = info.Job; @@ -206,7 +277,7 @@ namespace Barotrauma CanBeFocused = true }; - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); + nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); if (!info.OmitJobInMenus) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 655f5f79f..5a6ae2c90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -522,6 +522,11 @@ namespace Barotrauma } } + if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement) + { + crewManagement.RefreshUI(); + } + return background; } @@ -532,6 +537,10 @@ namespace Barotrauma crewList.RemoveChild(component); traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true)); } + if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement) + { + crewManagement.RefreshUI(); + } } private static void SetCharacterComponentTooltip(GUIComponent characterComponent) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 591003e89..e34b44a4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -344,18 +344,6 @@ namespace Barotrauma } } - protected SubmarineInfo GetPredefinedStartOutpost() - { - if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) - { - return new SubmarineInfo(parameters.OutpostFilePath.Value) - { - OutpostGenerationParams = parameters - }; - } - return null; - } - partial void NPCInteractProjSpecific(Character npc, Character interactor) { if (npc == null || interactor == null) { return; } @@ -370,7 +358,7 @@ namespace Barotrauma UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc); return; case InteractionType.Crew when GameMain.NetworkMember != null: - CampaignUI.CrewManagement.SendCrewState(false); + CampaignUI.HRManagerUI.SendCrewState(false); goto default; case InteractionType.MedicalClinic: CampaignUI.MedicalClinic.RequestLatestPending(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index eeeb558fd..7e6bc711f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -939,7 +939,16 @@ namespace Barotrauma UInt16 renamedIdentifier = msg.ReadUInt16(); string newName = msg.ReadString(); CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier); - if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); } + if (renamedCharacter != null) + { + CrewManager.RenameCharacter(renamedCharacter, newName); + // Since renaming can only be done once in permadeath, we can safely set this to false to disable the renaming in the UI. + renamedCharacter.RenamingEnabled = false; + } + else + { + DebugConsole.ThrowError($"Could not find a character to rename with the ID {renamedIdentifier}."); + } } bool fireCharacter = msg.ReadBoolean(); @@ -951,15 +960,15 @@ namespace Barotrauma if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } - if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.HRManagerUI != null) { //can't apply until we have the latest save file if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { - CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); - if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); } - CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); - if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } + CampaignUI.HRManagerUI.SetHireables(map.CurrentLocation, availableHires); + if (hiredCharacters.Any()) { CampaignUI.HRManagerUI.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); } + CampaignUI.HRManagerUI.SetPendingHires(pendingHires, map.CurrentLocation); + if (renameCrewMember || fireCharacter) { CampaignUI.HRManagerUI.UpdateCrew(); } } } else @@ -1007,6 +1016,11 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { + if (price == 0) + { + return true; + } + if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2f0b34bf4..3b43b5c1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1337,7 +1337,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); - campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); + campaign.CampaignUI?.HRManagerUI?.RefreshUI(); } } @@ -1552,7 +1552,7 @@ namespace Barotrauma.Networking } else { - GameMain.GameSession.StartRound(levelData, mirrorLevel); + GameMain.GameSession.StartRound(levelData, mirrorLevel, startOutpost: campaign?.GetPredefinedStartOutpost()); } isOutpost = levelData.Type == LevelData.LevelType.Outpost; } @@ -1957,7 +1957,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); - campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); + campaign.CampaignUI?.HRManagerUI?.RefreshUI(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 67be291c8..fb27297d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -39,7 +39,7 @@ namespace Barotrauma public CampaignMode Campaign { get; } - public CrewManagement CrewManagement { get; set; } + public HRManagerUI HRManagerUI { get; set; } public Store Store { get; private set; } @@ -102,7 +102,7 @@ namespace Barotrauma var crewTab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); tabs[(int)CampaignMode.InteractionType.Crew] = crewTab; - CrewManagement = new CrewManagement(this, crewTab); + HRManagerUI = new HRManagerUI(this, crewTab); // store tab ------------------------------------------------------------------------- @@ -204,7 +204,7 @@ namespace Barotrauma submarineSelection?.Update(); break; case CampaignMode.InteractionType.Crew: - CrewManagement?.Update(); + HRManagerUI?.Update(); break; case CampaignMode.InteractionType.Store: Store?.Update(deltaTime); @@ -598,8 +598,8 @@ namespace Barotrauma Store.SelectStore(npc); break; case CampaignMode.InteractionType.Crew: - CrewManagement.UpdateCrew(); - CrewManagement.UpdateHireables(); + HRManagerUI.UpdateCrew(); + HRManagerUI.UpdateHireables(); break; case CampaignMode.InteractionType.PurchaseSub: submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 3de5dac9e..ba7fc25c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1116,6 +1116,36 @@ namespace Barotrauma AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); + var newCharacterCostSliderElement = CreateLabeledSlider(settingsContent, + "ServerSettings.ReplaceCostPercentage", "", "ServerSettings.ReplaceCostPercentage.tooltip", + out var newCharacterCostSlider, out var newCharacterCostSliderLabel, + range: new Vector2(0, 200), step: 10f); + newCharacterCostSlider.StepValue = 10f; + newCharacterCostSlider.OnMoved = (GUIScrollBar scrollBar, float _) => + { + GUITextBlock textBlock = scrollBar.UserData as GUITextBlock; + int currentMultiplier = (int)Math.Round(scrollBar.BarScrollValue); + if (currentMultiplier < 1) + { + textBlock.Text = TextManager.Get("ServerSettings.ReplaceCostPercentage.Free"); + } + else + { + textBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", currentMultiplier.ToString()); + } + return true; + }; + newCharacterCostSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + clientDisabledElements.AddRange(newCharacterCostSliderElement.GetAllChildren()); + permadeathEnabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren()); + ironmanDisabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren()); + AssignComponentToServerSetting(newCharacterCostSlider, nameof(ServerSettings.ReplaceCostPercentage)); + newCharacterCostSlider.OnMoved(newCharacterCostSlider, newCharacterCostSlider.BarScroll); // initialize + var allowBotTakeoverTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("AllowBotTakeover")) { ToolTip = TextManager.Get("AllowBotTakeover.Tooltip"), @@ -1834,7 +1864,8 @@ namespace Barotrauma OverflowClip = true }; - if (PermanentlyDead) + if (!allowEditing || + (PermanentlyDead && !characterInfo.RenamingEnabled)) { CharacterNameBox.Readonly = true; CharacterNameBox.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 8957de022..7d91738cc 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 848c3c9b3..da41bd347 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 1455503dd..f28e3cf55 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 9005119d8..4e9e9584b 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 920854b33..3a47a3f9d 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index ec7154a5e..6b35b7d99 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -56,6 +56,7 @@ namespace Barotrauma msg.WriteUInt16(ID); msg.WriteString(Name); msg.WriteString(OriginalName); + msg.WriteBoolean(RenamingEnabled); msg.WriteByte((byte)Head.Preset.TagSet.Count); foreach (Identifier tag in Head.Preset.TagSet) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 4e1ef65eb..9d5bce1a6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -459,6 +459,7 @@ namespace Barotrauma Client owner = controlEventData.Owner; msg.WriteBoolean(owner == c && owner.Character == this); msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0); + msg.WriteBoolean(info is { RenamingEnabled: true }); break; case CharacterStatusEventData statusEventData: WriteStatus(msg, statusEventData.ForceAfflictionData); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 1f86f5297..f153d82d1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1231,6 +1231,10 @@ namespace Barotrauma renamedIdentifier = msg.ReadUInt16(); newName = msg.ReadString(); existingCrewMember = msg.ReadBoolean(); + if (!GameMain.Server.IsNameValid(sender, newName)) + { + renameCharacter = false; + } } bool fireCharacter = msg.ReadBoolean(); @@ -1239,10 +1243,11 @@ namespace Barotrauma Location location = map?.CurrentLocation; CharacterInfo firedCharacter = null; + (ushort id, string newName) appliedRename = (Entity.NullEntityID, string.Empty); - if (location != null && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) + if (location != null) { - if (fireCharacter) + if (fireCharacter && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier); if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true)) @@ -1258,29 +1263,45 @@ namespace Barotrauma if (renameCharacter) { CharacterInfo characterInfo = null; - if (existingCrewMember && CrewManager != null) + if (AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { - characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier); + if (existingCrewMember && CrewManager != null) + { + characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier); + } + else if (!existingCrewMember && location.HireManager != null) + { + characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier); + } } - else if(!existingCrewMember && location.HireManager != null) + if (characterInfo == null && renamedIdentifier == sender.CharacterInfo?.ID) { - characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier); + characterInfo = sender.CharacterInfo; } - - if (characterInfo != null && (characterInfo.Character?.IsBot ?? true)) + if (characterInfo != null && + (characterInfo.Character == null || characterInfo.Character is { IsBot: true } || (characterInfo.RenamingEnabled && characterInfo == sender.CharacterInfo))) { + GameServer.Log($"{sender.Name} renamed the character \"{characterInfo.Name}\" as \"{newName}\".", ServerLog.MessageType.ServerMessage); if (existingCrewMember) { CrewManager.RenameCharacter(characterInfo, newName); + if (characterInfo == sender.CharacterInfo) + { + //renaming is only allowed once + characterInfo.RenamingEnabled = false; + } } else { location.HireManager.RenameCharacter(characterInfo, newName); } + appliedRename = (characterInfo.ID, newName); } else { - DebugConsole.ThrowError($"Tried to rename an invalid character ({renamedIdentifier})"); + string errorMsg = $"Tried to rename an invalid character ({renamedIdentifier}, {characterInfo?.Name ?? "null"})"; + DebugConsole.ThrowError(errorMsg); + GameMain.Server?.SendConsoleMessage(errorMsg, sender, Color.Red); } } @@ -1328,7 +1349,7 @@ namespace Barotrauma // bounce back if (renameCharacter && existingCrewMember) { - SendCrewState((renamedIdentifier, newName), firedCharacter); + SendCrewState(appliedRename, firedCharacter); } else { @@ -1406,6 +1427,8 @@ namespace Barotrauma //(can happen e.g. if someone starts a vote to buy something and then disconnects) if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; } + if (price == 0) { return true; } + Wallet wallet = GetWallet(client); if (!AllowedToManageWallets(client)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index c5e231d82..e7f4cd5d6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -313,13 +313,22 @@ namespace Barotrauma.Networking GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red); return false; } - - // Now that the old permanently killed character will be replaced, we can fully discard it - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + + if (botCharacter.Info != null) { - mpCampaign.DiscardClientCharacterData(this); + botCharacter.Info.RenamingEnabled = true; // Grant one opportunity to rename a taken over bot } + + // Now that the old permanently killed character will be replaced, we can fully discard it + var mpCampaign = GameMain.GameSession?.Campaign as MultiPlayerCampaign; + mpCampaign?.DiscardClientCharacterData(this); GameMain.Server.SetClientCharacter(this, botCharacter); + if (mpCampaign?.SetClientCharacterData(this) is CharacterCampaignData characterData) + { + //the bot has spawned, but the new CharacterCampaignData technically hasn't, because we just created it + characterData.HasSpawned = true; + } + SpectateOnly = false; return true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 14cd1795f..d221ecedf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1394,16 +1394,26 @@ namespace Barotrauma.Networking if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter) { - if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender)) + if (ServerSettings.ReplaceCostPercentage <= 0 || + CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMoney) || + CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { - campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter); - SpawnAndTakeOverBot(campaign, hireableCharacter, sender); - campaign.SendCrewState(createNotification: false); + if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender, buyingNewCharacter: true)) + { + campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter); + SpawnAndTakeOverBot(campaign, hireableCharacter, sender); + campaign.SendCrewState(createNotification: false); + } + else + { + SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red); + DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}."); + } } else { - SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red); - DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}."); + SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}. No permission to manage money or hires.", sender, Color.Red); + DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}. No permission to manage money or hires."); } } else @@ -1448,6 +1458,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("SpawnAndTakeOverBot: newCharacter is null somehow"); return; } + // No longer show the hired character in the HR list of current hires campaign.CrewManager.RemoveCharacterInfo(botInfo); newCharacter.TeamID = CharacterTeamType.Team1; campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint); @@ -2407,7 +2418,7 @@ namespace Barotrauma.Networking } SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize: false); - GameMain.GameSession.StartRound(campaign.NextLevel, mirrorLevel: campaign.MirrorLevel); + GameMain.GameSession.StartRound(campaign.NextLevel, startOutpost: campaign.GetPredefinedStartOutpost(), mirrorLevel: campaign.MirrorLevel); SubmarineSwitchLoad = false; campaign.AssignClientCharacterInfos(connectedClients); Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); @@ -2994,7 +3005,7 @@ namespace Barotrauma.Networking } } - private bool IsNameValid(Client c, string newName) + public bool IsNameValid(Client c, string newName) { newName = Client.SanitizeName(newName); @@ -3018,13 +3029,20 @@ namespace Barotrauma.Networking } } - Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); - if (nameTaken != null) + Client nameTakenByClient = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); + if (nameTakenByClient != null) { - SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTaken.Name}", c, ChatMessageType.ServerMessageBox); + SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByClient.Name}", c, ChatMessageType.ServerMessageBox); return false; } + Character nameTakenByCharacter = + GameSession.GetSessionCrewCharacters(CharacterType.Both).FirstOrDefault(c2 => c2 != c.Character && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); + if (nameTakenByCharacter != null) + { + SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByCharacter.Name}", c, ChatMessageType.ServerMessageBox); + return false; + } return true; } @@ -3767,6 +3785,7 @@ namespace Barotrauma.Networking newCharacter.SetOwnerClient(client); newCharacter.Enabled = true; client.Character = newCharacter; + client.CharacterInfo = newCharacter.Info; CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 31a1cd1f7..537fd1635 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.8.0 + 1.5.9.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs index 741fe381d..931215416 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs @@ -1,4 +1,6 @@ #nullable enable +using FarseerPhysics; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -117,24 +119,52 @@ namespace Barotrauma { if (inspectTimer > 0.0f) { - character.SelectCharacter(Target); + Vector2 diff = Target.WorldPosition - character.WorldPosition; + float dist = diff.Length(); + float maxDist = ConvertUnits.ToDisplayUnits(HumanoidAnimController.BreakFromGrabDistance); + if (dist > maxDist) + { + if (dist > maxDist * 2 || !character.CanSeeTarget(Target, seeThroughWindows: false)) + { + //too far to reach by small manual movement, need to switch back to the earlier state + currentState = State.GotoTarget; + } + //move closer horizontally if the horizontal distance is the issue + else if (Math.Abs(diff.X) > Math.Abs(diff.Y) * 2.0f) + { + character.AIController.SteeringManager.SteeringManual(deltaTime, new Vector2(MathF.Sign(Target.WorldPosition.X - character.WorldPosition.X), 0.0f)); + } + else + { + character.AIController.SteeringManager.Reset(); + } + return; + } + else + { + if (dist < maxDist * 0.5f) { character.AIController.SteeringManager.Reset(); } + character.SelectCharacter(Target); + } + inspectTimer -= deltaTime; if (inspectTimer < InspectTime - 1) { - if (Target.AnimController.IsMovingFast) + if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f) { - ArrestFleeing(); - } - else if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f) - { - // If the target moves, reset the inspect timer and tell to hold still + // If the target moves, tell to hold still character.Speak(TextManager.Get("dialogcheckstolenitems.holdstill").Value, identifier: "holdstill".ToIdentifier(), minDurationBetweenSimilar: 3f); - inspectTimer = InspectTime; } } return; } + if (character.SelectedCharacter != Target) + { + //target not selected -> must've escaped + Abandon = true; + return; + } + if (stolenItems.Any() && Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability) { @@ -155,13 +185,6 @@ namespace Barotrauma if (warnTimer > 0.0f) { warnTimer -= deltaTime; - if (warnTimer < currentWarnDelay - 1) - { - if (Target.AnimController.IsMovingFast) - { - ArrestFleeing(); - } - } return; } var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target); @@ -189,14 +212,6 @@ namespace Barotrauma IsCompleted = true; } - private void ArrestFleeing() - { - character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value); - currentState = State.Done; - IsCompleted = true; - Arrest(abortWhenItemsDropped: false, allowHoldFire: false); - } - private void Arrest(bool abortWhenItemsDropped, bool allowHoldFire) { bool isCriminal = Target.IsCriminal; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index f59030353..43a0197ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -12,6 +12,8 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 0.2f; public bool TargetCharactersInOtherSubs { get; init; } + protected override bool AllowInAnySub => TargetCharactersInOtherSubs; + public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 6024c582a..14c49042d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -21,6 +21,8 @@ namespace Barotrauma private const float MaxSpeedOnStairs = 1.7f; private const float SteepSlopePushMagnitude = MaxSpeedOnStairs; + public const float BreakFromGrabDistance = 1.4f; + public override RagdollParams RagdollParams { get { return HumanRagdollParams; } @@ -1839,7 +1841,7 @@ namespace Barotrauma float dist = ConvertUnits.ToSimUnits(Vector2.Distance(target.WorldPosition, WorldPosition)); //let the target break free if it's moving away and gets far enough - if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && dist > 1.4f && target.AllowInput && + if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && dist > BreakFromGrabDistance && target.AllowInput && Vector2.Dot(target.WorldPosition - WorldPosition, target.AnimController.TargetMovement) > 0) { character.DeselectCharacter(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 0d6aefde0..3706c4360 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -299,6 +299,7 @@ namespace Barotrauma public XElement OrderData; public bool PermanentlyDead; + public bool RenamingEnabled = false; private static ushort idCounter = 1; private const string disguiseName = "???"; @@ -802,6 +803,7 @@ namespace Barotrauma LoadTagsBackwardsCompatibility(infoElement, tags); SpeciesName = infoElement.GetAttributeIdentifier("speciesname", ""); PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false); + RenamingEnabled = infoElement.GetAttributeBool("renamingenabled", false); ContentXElement element; if (!SpeciesName.IsEmpty) { @@ -1499,7 +1501,8 @@ namespace Barotrauma new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty), new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()), - new XAttribute("permanentlydead", PermanentlyDead) + new XAttribute("permanentlydead", PermanentlyDead), + new XAttribute("renamingenabled", RenamingEnabled) ); if (HumanPrefabIds != default) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index f247879bd..649b15905 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -7,9 +7,14 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { + + /// + /// Responsible for keeping track of the characters in the player crew, saving and loading their orders, managing the crew list UI + /// partial class CrewManager { const float ConversationIntervalMin = 100.0f; @@ -27,7 +32,10 @@ namespace Barotrauma { return characters; } - + /// + /// Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firing/renaming, which only applies to AI characters. + /// Use to get all the characters regardless if they're player or AI controlled. + /// public IEnumerable GetCharacterInfos() { return characterInfos; @@ -387,15 +395,8 @@ namespace Barotrauma public void RenameCharacter(CharacterInfo characterInfo, string newName) { - int identifier = characterInfo.GetIdentifierUsingOriginalName(); - var match = characterInfos.FirstOrDefault(ci => ci.GetIdentifierUsingOriginalName() == identifier); - if (match == null) - { - DebugConsole.ThrowError($"Tried to rename an invalid crew member ({identifier})"); - return; - } - match.Rename(newName); - RenameCharacterProjSpecific(match); + characterInfo.Rename(newName); + RenameCharacterProjSpecific(characterInfo); } partial void RenameCharacterProjSpecific(CharacterInfo characterInfo); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index eee7b7643..837cdc7ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -211,7 +211,7 @@ namespace Barotrauma public virtual bool TryPurchase(Client client, int price) { - return GetWallet(client).TryDeduct(price); + return price == 0 || GetWallet(client).TryDeduct(price); } public virtual int GetBalance(Client client = null) @@ -250,6 +250,19 @@ namespace Barotrauma (sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit)); } + public SubmarineInfo GetPredefinedStartOutpost() + { + if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && + !parameters.OutpostFilePath.IsNullOrEmpty()) + { + return new SubmarineInfo(parameters.OutpostFilePath.Value) + { + OutpostGenerationParams = parameters + }; + } + return null; + } + public override void Start() { base.Start(); @@ -1044,7 +1057,7 @@ namespace Barotrauma return ToolBox.SelectWeightedRandom(factionsList, weights, random); } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney = true, Client client = null) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney = true, Client client = null, bool buyingNewCharacter = false) { if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) @@ -1054,7 +1067,8 @@ namespace Barotrauma return false; } } - if (takeMoney && !TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } + var price = buyingNewCharacter ? NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo); + if (takeMoney && !TryPurchase(client, price)) { return false; } characterInfo.IsNewHire = true; characterInfo.Title = null; @@ -1063,6 +1077,17 @@ namespace Barotrauma GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); return true; } + + public int NewCharacterCost(CharacterInfo characterInfo) + { + float characterCostPercentage = GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage ?? 100f; + return (int)MathF.Round(HireManager.GetSalaryFor(characterInfo) * (characterCostPercentage/100f)); + } + + public bool CanAffordNewCharacter(CharacterInfo characterInfo) + { + return CanAfford(NewCharacterCost(characterInfo)); + } private void NPCInteract(Character npc, Character interactor) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index a79c24188..171d4b9b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -574,6 +574,12 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); } + if (GameMain.NetworkMember?.ServerSettings is { } serverSettings) + { + GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode); + GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanMode); + GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath); + } } #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index d6e9cc668..f89a9b55e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -475,6 +475,16 @@ namespace Barotrauma.Networking get; private set; } + + [Serialize(100f, IsPropertySaveable.Yes)] + /// + /// Percentage modifier for the cost of hiring a new character to replace a permanently killed one. + /// + public float ReplaceCostPercentage + { + get; + private set; + } [Serialize(true, IsPropertySaveable.Yes)] /// @@ -647,6 +657,8 @@ namespace Barotrauma.Networking set { if (respawnMode == value) { return; } + //can't change this when a round is running (but clients can, if the server says so, e.g. when a client joins and needs to know what it's set to despite a round being running) + if (GameMain.NetworkMember is { GameStarted: true, IsServer: true }) { return; } respawnMode = value; ServerDetailsChanged = true; } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 432b16ae8..91c290fc8 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,17 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.5.9.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed outpost security being way too eager to arrest characters who attempt to escape a security inspection, to the point that getting inspected while running past a guard would immediately make them arrest you. +- Fixed some thalamus items being hidden for no reason in some wrecks. +- Fixed "assault enemy" order not working in abandoned/enemy outposts. +- Added a configurable multiplier for the cost of hiring a new character to replace a permanently killed one. Makes it possible to host permadeath servers without having to assign hiring or money permissions to clients. +- Players are allowed to rename the character they take over in the permadeath mode. + +Modding: +- Fixed forceOutpostGenerationParamsIdentifier (which can be used to force specific outpost generation params to be used in a location type) only working in single player. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.5.8.0 -------------------------------------------------------------------------------------------------------------------------------------------------