(965c31410a) Unstable v0.10.4.0

This commit is contained in:
Juan Pablo Arce
2020-07-21 08:57:50 -03:00
parent 4f8bd39789
commit 33d3a41104
546 changed files with 45952 additions and 25762 deletions
@@ -91,19 +91,19 @@ namespace Barotrauma
FocusedCharacter = null;
}
var closestEntity = FindEntityByID(memInput[memInput.Count - 1].interact);
if (closestEntity is Item)
if (closestEntity is Item item)
{
if (CanInteractWith((Item)closestEntity))
if (CanInteractWith(item))
{
focusedItem = (Item)closestEntity;
focusedItem = item;
FocusedCharacter = null;
}
}
else if (closestEntity is Character)
else if (closestEntity is Character character)
{
if (CanInteractWith((Character)closestEntity))
if (CanInteractWith(character, maxDist: 250.0f))
{
FocusedCharacter = (Character)closestEntity;
FocusedCharacter = character;
focusedItem = null;
}
}
@@ -207,6 +207,13 @@ namespace Barotrauma
{
LastNetworkUpdateID = networkUpdateID;
}
else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500)
{
#if DEBUG || UNSTABLE
DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID.");
#endif
LastNetworkUpdateID = networkUpdateID;
}
if (memInput.Count > 60)
{
//deleting inputs from the queue here means the server is way behind and data needs to be dropped
@@ -264,21 +271,21 @@ namespace Barotrauma
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.WriteRangedInteger(0, 0, 4);
msg.WriteRangedInteger(0, 0, 5);
msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
Inventory.ServerWrite(msg, c);
break;
case NetEntityEvent.Type.Control:
msg.WriteRangedInteger(1, 0, 4);
msg.WriteRangedInteger(1, 0, 5);
Client owner = (Client)extraData[1];
msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0);
break;
case NetEntityEvent.Type.Status:
msg.WriteRangedInteger(2, 0, 4);
msg.WriteRangedInteger(2, 0, 5);
WriteStatus(msg);
break;
case NetEntityEvent.Type.UpdateSkills:
msg.WriteRangedInteger(3, 0, 4);
msg.WriteRangedInteger(3, 0, 5);
if (Info?.Job == null)
{
msg.Write((byte)0);
@@ -297,11 +304,15 @@ namespace Barotrauma
Limb attackLimb = extraData[1] as Limb;
UInt16 targetEntityID = (UInt16)extraData[2];
int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0;
msg.WriteRangedInteger(4, 0, 4);
msg.WriteRangedInteger(4, 0, 5);
msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb)));
msg.Write(targetEntityID);
msg.Write((byte)targetLimbIndex);
break;
case NetEntityEvent.Type.AssignCampaignInteraction:
msg.WriteRangedInteger(5, 0, 5);
msg.Write((byte)CampaignInteractionType);
break;
default:
DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")");
break;
@@ -507,6 +518,8 @@ namespace Barotrauma
msg.Write(info.SpeciesName);
info.ServerWrite(msg);
msg.Write((byte)CampaignInteractionType);
// Current order
if (info.CurrentOrder != null)
{
@@ -10,6 +10,7 @@ using System.Threading;
using Barotrauma.IO;
using System.Text;
using System.Diagnostics;
using System.Globalization;
namespace Barotrauma
{
@@ -1213,7 +1214,7 @@ namespace Barotrauma
if (int.TryParse(string.Join(" ", args), out index))
{
if (index > 0 && index < GameMain.NetLobbyScreen.GameModes.Length &&
GameMain.NetLobbyScreen.GameModes[index].Identifier == "multiplayercampaign")
GameMain.NetLobbyScreen.GameModes[index] == GameModePreset.MultiPlayerCampaign)
{
MultiPlayerCampaign.StartCampaignSetup();
}
@@ -1541,6 +1542,11 @@ namespace Barotrauma
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client);
return;
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
@@ -2056,6 +2062,54 @@ namespace Barotrauma
NewMessage(tag, Color.Yellow);
}
}));
AssignOnClientRequestExecute(
"setskill",
(senderClient, cursorWorldPos, args) =>
{
if (args.Length < 2)
{
GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", senderClient);
return;
}
string skillIdentifier = args[0];
string levelString = args[1];
Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : senderClient.Character;
if (character?.Info?.Job == null)
{
GameMain.Server.SendConsoleMessage("Character is not valid.", senderClient);
return;
}
bool isMax = levelString.Equals("max", StringComparison.OrdinalIgnoreCase);
if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax)
{
if (isMax) { level = 100; }
if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase))
{
foreach (Skill skill in character.Info.Job.Skills)
{
character.Info.SetSkillLevel(skill.Identifier, level, character.WorldPosition);
}
GameMain.Server.SendConsoleMessage($"Set all {character.Name}'s skills to {level}", senderClient);
}
else
{
character.Info.SetSkillLevel(skillIdentifier, level, character.WorldPosition);
GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient);
}
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdateSkills });
}
else
{
GameMain.Server.SendConsoleMessage($"{levelString} is not a valid level. Expected number or \"max\".", senderClient);
}
}
);
#if DEBUG
commands.Add(new Command("spamevents", "A debug command that creates a ton of entity events.", (string[] args) =>
@@ -0,0 +1,115 @@
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma
{
partial class ConversationAction : EventAction
{
public int SelectedOption
{
get { return selectedOption; }
set { selectedOption = value; }
}
private static readonly Dictionary<Client, ConversationAction> lastActiveAction = new Dictionary<Client, ConversationAction>();
private readonly HashSet<Client> targetClients = new HashSet<Client>();
public IEnumerable<Client> TargetClients
{
get { return targetClients; }
}
private bool IsBlockedByAnotherConversation(IEnumerable<Entity> targets)
{
foreach (Entity e in targets)
{
if (!(e is Character character) || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null)
{
if (lastActiveAction.ContainsKey(targetClient) &&
lastActiveAction[targetClient].ParentEvent != ParentEvent &&
Timing.TotalTime < lastActiveAction[targetClient].lastActiveTime + BlockOtherConversationsDuration)
{
return true;
}
}
}
return false;
}
partial void ShowDialog(Character speaker, Character targetCharacter)
{
targetClients.Clear();
if (!string.IsNullOrEmpty(TargetTag))
{
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
foreach (Entity e in entities)
{
if (!(e is Character character) || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null)
{
targetClients.Add(targetClient);
lastActiveAction[targetClient] = this;
ServerWrite(speaker, targetClient);
}
}
}
else
{
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null)
{
if (targetCharacter == null || targetCharacter == c.Character)
{
targetClients.Add(c);
lastActiveAction[c] = this;
ServerWrite(speaker, c);
}
}
}
}
}
private void ServerWrite(Character speaker, Client client)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte)ServerPacketHeader.EVENTACTION);
outmsg.Write((byte)EventManager.NetworkEventType.CONVERSATION);
outmsg.Write(Identifier);
outmsg.Write(EventSprite);
outmsg.Write((byte)DialogType);
outmsg.Write(ContinueConversation);
if (interrupt)
{
outmsg.Write(speaker?.ID ?? Entity.NullEntityID);
outmsg.Write(string.Empty);
outmsg.Write(false);
outmsg.Write((byte)0);
outmsg.Write((byte)0);
}
else
{
outmsg.Write(speaker?.ID ?? Entity.NullEntityID);
outmsg.Write(Text ?? string.Empty);
outmsg.Write(FadeToBlack);
outmsg.Write((byte)Options.Count);
for (int i = 0; i < Options.Count; i++)
{
outmsg.Write(Options[i].Text);
}
int[] endings = GetEndingOptions();
outmsg.Write((byte)endings.Length);
foreach (var end in endings)
{
outmsg.Write((byte)end);
}
}
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
}
@@ -0,0 +1,28 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class StatusEffectAction : EventAction
{
private void ServerWrite(IEnumerable<Entity> targets)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte)ServerPacketHeader.EVENTACTION);
outmsg.Write((byte)EventManager.NetworkEventType.STATUSEFFECT);
outmsg.Write(ParentEvent.Prefab.Identifier);
outmsg.Write((UInt16)actionIndex);
outmsg.Write((UInt16)targets.Count());
foreach (Entity target in targets)
{
outmsg.Write(target.ID);
}
foreach (Client c in GameMain.Server.ConnectedClients)
{
GameMain.Server.ServerPeer?.Send(outmsg, c.Connection, DeliveryMethod.Reliable);
}
}
}
}
@@ -0,0 +1,37 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class EventManager
{
public void ServerRead(IReadMessage inc, Client sender)
{
UInt16 actionId = inc.ReadUInt16();
byte selectedOption = inc.ReadByte();
foreach (Event ev in activeEvents)
{
if (!(ev is ScriptedEvent scriptedEvent)) { continue; }
var actions = FindActions(scriptedEvent);
foreach (EventAction action in actions.Select(a => a.Item2))
{
if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; }
if (!convAction.TargetClients.Contains(sender))
{
#if DEBUG || UNSTABLE
DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them.");
#endif
continue;
}
convAction.SelectedOption = selectedOption;
return;
}
}
}
}
}
@@ -10,8 +10,9 @@ namespace Barotrauma
foreach (Item item in items)
{
item.WriteSpawnData(msg,
itemIDs[item],
parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID);
item.OriginalID,
parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID,
parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0);
}
}
}
@@ -7,7 +7,7 @@ namespace Barotrauma
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
if (monsters.Count == 0 && monsterFiles.Count > 0)
if (monsters.Count == 0 && monsterPrefabs.Count > 0)
{
throw new InvalidOperationException("Server attempted to write monster mission data when no monsters had been spawned.");
}
@@ -8,8 +8,8 @@ namespace Barotrauma
{
private bool usedExistingItem;
private UInt16 originalItemID;
private UInt16 originalInventoryID;
private byte originalItemContainerIndex;
private readonly List<Pair<int, int>> executedEffectIndices = new List<Pair<int, int>>();
@@ -18,11 +18,11 @@ namespace Barotrauma
msg.Write(usedExistingItem);
if (usedExistingItem)
{
msg.Write(originalItemID);
msg.Write(item.OriginalID);
}
else
{
item.WriteSpawnData(msg, originalItemID, originalInventoryID);
item.WriteSpawnData(msg, item.OriginalID, originalInventoryID, originalItemContainerIndex);
}
msg.Write((byte)executedEffectIndices.Count);
@@ -98,20 +98,23 @@ namespace Barotrauma
public void Init()
{
NPCSet.LoadSets();
FactionPrefab.LoadFactions();
CharacterPrefab.LoadAll();
MissionPrefab.Init();
TraitorMissionPrefab.Init();
MapEntityPrefab.Init();
MapGenerationParams.Init();
LevelGenerationParams.LoadPresets();
ScriptedEventSet.LoadPrefabs();
OutpostGenerationParams.LoadPresets();
EventSet.LoadPrefabs();
Order.Init();
EventManagerSettings.Init();
AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions));
SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings));
StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure));
ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item));
UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules));
JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs));
CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses));
NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations));
@@ -344,7 +347,10 @@ namespace Barotrauma
{
Timing.TotalTime += Timing.Step;
DebugConsole.Update();
Screen.Selected?.Update((float)Timing.Step);
if (GameSession?.GameMode == null || !GameSession.GameMode.Paused)
{
Screen.Selected?.Update((float)Timing.Step);
}
Server.Update((float)Timing.Step);
if (Server == null) { break; }
SteamManager.Update((float)Timing.Step);
@@ -0,0 +1,57 @@
using System.Collections.Generic;
namespace Barotrauma
{
partial class CargoManager
{
public void SellBackPurchasedItems(List<PurchasedItem> itemsToSell)
{
foreach (PurchasedItem item in itemsToSell)
{
var itemValue = GetBuyValueAtCurrentLocation(item);
campaign.Map.CurrentLocation.StoreCurrentBalance -= itemValue;
campaign.Money += itemValue;
PurchasedItems.Remove(item);
}
}
public void BuyBackSoldItems(List<SoldItem> itemsToBuy)
{
foreach (SoldItem item in itemsToBuy)
{
var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab);
if (location.StoreCurrentBalance < itemValue || item.Removed) { continue; }
location.StoreCurrentBalance += itemValue;
campaign.Money -= itemValue;
SoldItems.Remove(item);
}
}
public void SellItems(List<SoldItem> itemsToSell)
{
var canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null;
foreach (SoldItem item in itemsToSell)
{
var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab);
// check if the store can afford the item and if the item hasn't been removed already
if (location.StoreCurrentBalance < itemValue || item.Removed) { continue; }
if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity)
{
item.Removed = true;
Entity.Spawner.AddToRemoveQueue(entity);
}
SoldItems.Add(item);
location.StoreCurrentBalance -= itemValue;
campaign.Money += itemValue;
}
OnSoldItemsChanged?.Invoke();
}
public void ClearSoldItemsProjSpecific()
{
SoldItems.Clear();
}
}
}
@@ -1,5 +1,8 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -19,5 +22,23 @@ namespace Barotrauma
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
/// <summary>
/// Saves bots in multiplayer
/// </summary>
/// <param name="root"></param>
public void SaveMultiplayer(XElement root)
{
XElement saveElement = new XElement("bots", new XAttribute("hasbots", HasBots));
foreach (CharacterInfo info in characterInfos)
{
if (info?.Character == null || info.Character.IsDead) { continue; }
XElement characterElement = info.Save(saveElement);
if (info.InventoryData != null) { characterElement.Add(info.InventoryData); }
if (info.HealthData != null) { characterElement.Add(info.HealthData); }
}
root.Add(saveElement);
}
}
}
@@ -1,15 +1,22 @@
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma
{
abstract partial class CampaignMode : GameMode
{
public bool MirrorLevel
{
get;
protected set;
}
public override void ShowStartMessage()
{
if (Mission == null) return;
Networking.GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage);
Networking.GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage);
GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage);
GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage);
}
}
}
@@ -25,9 +25,19 @@ namespace Barotrauma
}
}
public bool IsDuplicate(CharacterCampaignData other)
{
return other.SteamID == SteamID && other.ClientEndPoint == ClientEndPoint;
}
public void SpawnInventoryItems(CharacterInfo characterInfo, Inventory inventory)
{
characterInfo.SpawnInventoryItems(inventory, itemData);
}
public void ApplyHealthData(CharacterInfo characterInfo, Character character)
{
characterInfo.ApplyHealthData(character, healthData);
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -10,36 +11,57 @@ namespace Barotrauma
{
partial class MultiPlayerCampaign : CampaignMode
{
private List<CharacterCampaignData> characterData = new List<CharacterCampaignData>();
private readonly List<CharacterCampaignData> characterData = new List<CharacterCampaignData>();
private bool forceMapUI;
public bool ForceMapUI
{
get { return forceMapUI; }
set
{
if (forceMapUI == value) { return; }
forceMapUI = value;
LastUpdateID++;
}
}
public bool GameOver { get; private set; }
public override bool Paused
{
get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); }
}
public static void StartNewCampaign(string savePath, string subPath, string seed)
{
if (string.IsNullOrWhiteSpace(savePath)) return;
GameMain.GameSession = new GameSession(new SubmarineInfo(subPath, ""), savePath,
GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign"));
var campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode);
campaign.GenerateMap(seed);
campaign.SetDelegates();
if (string.IsNullOrWhiteSpace(savePath)) { return; }
GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, seed);
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
GameMain.GameSession.Map.SelectRandomLocation(true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
campaign.LastSaveID++;
DebugConsole.NewMessage("Campaign started!", Color.Cyan);
DebugConsole.NewMessage(GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan);
DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.Name, Color.Cyan);
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel();
}
public static void LoadCampaign(string selectedSave)
{
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
SaveUtil.LoadGame(selectedSave);
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LastSaveID++;
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
GameMain.GameSession.Map.SelectRandomLocation(true);
DebugConsole.NewMessage("Campaign loaded!", Color.Cyan);
DebugConsole.NewMessage(GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan);
DebugConsole.NewMessage(
GameMain.GameSession.Map.SelectedLocation == null ?
GameMain.GameSession.Map.CurrentLocation.Name :
GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan);
}
protected override void LoadInitialLevel()
{
NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData;
MirrorLevel = false;
GameMain.Server.StartGame();
}
public static void StartCampaignSetup()
@@ -57,7 +79,7 @@ namespace Barotrauma
}
else
{
var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer).ToArray();
var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray();
if (saveFiles.Length == 0)
{
DebugConsole.ThrowError("No save files found.");
@@ -86,53 +108,206 @@ namespace Barotrauma
});
}
public bool AllowedToEndRound(Character interactor)
public override void Start()
{
if (interactor == null || Level.Loaded?.StartOutpost == null || Level.Loaded?.EndOutpost == null)
{
return false;
}
if (interactor.Submarine == Level.Loaded.StartOutpost &&
interactor.CanInteractWith(startWatchman))
{
return true;
}
if (interactor.Submarine == Level.Loaded.EndOutpost &&
interactor.CanInteractWith(endWatchman))
{
return true;
}
return false;
base.Start();
lastUpdateID++;
}
protected override void WatchmanInteract(Character watchman, Character interactor)
{
if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) ||
(watchman.Submarine == Level.Loaded.EndOutpost && !Submarine.MainSub.AtEndPosition))
{
CreateDialog(new List<Character> { watchman }, "WatchmanInteractNoLeavingSub", 5.0f);
return;
}
private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection;
bool hasPermissions = true;
if (GameMain.Server != null)
{
var client = GameMain.Server.ConnectedClients.Find(c => c.Character == interactor);
hasPermissions = client != null;
CreateDialog(new List<Character> { watchman }, hasPermissions ? "WatchmanInteract" : "WatchmanInteractNotAllowed", 1.0f);
}
/// <summary>
/// There is a client-side implementation of the method in <see cref="CampaignMode"/>
/// </summary>
public bool AllowedToEndRound(Client client)
{
//allow ending the round if the client has permissions, is the owner, the only client in the server,
//or if no-one has permissions
return
client.HasPermission(ClientPermissions.ManageRound) ||
client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Server.ConnectedClients.Count == 1 ||
IsOwner(client) ||
GameMain.Server.ConnectedClients.None(c =>
c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign)));
}
partial void SetDelegates()
/// <summary>
/// There is a client-side implementation of the method in <see cref="CampaignMode"/>
/// </summary>
public bool AllowedToManageCampaign(Client client)
{
//allow ending the round if the client has permissions, is the owner, or the only client in the server,
//or if no-one has management permissions
return
client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Server.ConnectedClients.Count == 1 ||
IsOwner(client) ||
GameMain.Server.ConnectedClients.None(c =>
c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign)));
}
protected override IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults)
{
lastUpdateID++;
switch (transitionType)
{
case TransitionType.None:
throw new InvalidOperationException("Level transition failed (no transitions available).");
case TransitionType.ReturnToPreviousLocation:
//deselect destination on map
map.SelectLocation(-1);
break;
case TransitionType.ProgressToNextLocation:
Map.MoveToNextLocation();
Map.ProgressWorld();
break;
case TransitionType.End:
EndCampaign();
IsFirstRound = true;
break;
}
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
GameMain.GameSession.EndRound("", traitorResults, transitionType);
//--------------------------------------
if (success)
{
List<CharacterCampaignData> prevCharacterData = new List<CharacterCampaignData>(characterData);
//client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has a character)
characterData.RemoveAll(cd => cd.HasSpawned);
//refresh the character data of clients who are still in the server
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character?.Info == null) { continue; }
if (c.Character.IsDead && c.Character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { continue; }
c.CharacterInfo = c.Character.Info;
characterData.RemoveAll(cd => cd.MatchesClient(c));
characterData.Add(new CharacterCampaignData(c));
}
//refresh the character data of clients who aren't in the server anymore
foreach (CharacterCampaignData data in prevCharacterData)
{
if (data.HasSpawned && !characterData.Any(cd => cd.IsDuplicate(data)))
{
var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo);
if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected))
{
data.Refresh(character);
characterData.Add(data);
}
}
}
characterData.ForEach(cd => cd.HasSpawned = false);
//remove all items that are in someone's inventory
foreach (Character c in Character.CharacterList)
{
if (c.Inventory == null) { continue; }
if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost)
{
Map.CurrentLocation.RegisterTakenItems(c.Inventory.Items.Where(it => it != null && it.SpawnedInOutpost && it.OriginalModuleIndex > 0).Distinct());
}
if (c.Info != null && c.IsBot)
{
if (c.IsDead && c.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { CrewManager.RemoveCharacterInfo(c.Info); }
c.Info.HealthData = new XElement("health");
c.CharacterHealth.Save(c.Info.HealthData);
c.Info.InventoryData = new XElement("inventory");
c.SaveInventory(c.Inventory, c.Info.InventoryData);
}
c.Inventory.DeleteAllItems();
}
yield return CoroutineStatus.Running;
if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
{
Submarine.MainSub = leavingSub;
GameMain.GameSession.Submarine = leavingSub;
var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
foreach (Submarine sub in subsToLeaveBehind)
{
MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
LinkedSubmarine.CreateDummy(leavingSub, sub);
}
}
NextLevel = newLevel;
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else
{
GameMain.Server.EndGame(TransitionType.None);
LoadCampaign(GameMain.GameSession.SavePath);
LastSaveID++;
LastUpdateID++;
yield return CoroutineStatus.Success;
}
//--------------------------------------
GameMain.Server.EndGame(transitionType);
ForceMapUI = false;
NextLevel = newLevel;
MirrorLevel = mirror;
if (PendingSubmarineSwitch != null)
{
SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo;
GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch;
PendingSubmarineSwitch = null;
for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
{
if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name)
{
GameMain.GameSession.OwnedSubmarines[i] = previousSub;
}
}
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
LastSaveID++;
}
//give clients time to play the end cinematic before starting the next round
if (transitionType == TransitionType.End)
{
yield return new WaitForSeconds(EndCinematicDuration);
}
else
{
yield return new WaitForSeconds(EndTransitionDuration * 0.5f);
}
GameMain.Server.StartGame();
yield return CoroutineStatus.Success;
}
partial void InitProjSpecific()
{
if (GameMain.Server != null)
{
CargoManager.OnItemsChanged += () => { LastUpdateID++; };
CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; };
CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; };
CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; };
UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; };
Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; };
Map.OnMissionSelected += (loc, mission) => { LastUpdateID++; };
}
//increment save ID so clients know they're lacking the most up-to-date save file
LastSaveID++;
}
public void DiscardClientCharacterData(Client client)
@@ -144,14 +319,22 @@ namespace Barotrauma
{
return characterData.Find(cd => cd.MatchesClient(client));
}
public CharacterCampaignData SetClientCharacterData(Client client)
{
characterData.RemoveAll(cd => cd.MatchesClient(client));
var data = new CharacterCampaignData(client);
characterData.Add(data);
return data;
}
public void AssignClientCharacterInfos(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
if (client.SpectateOnly && GameMain.Server.ServerSettings.AllowSpectating) { continue; }
var matchingData = GetClientCharacterData(client);
if (matchingData != null) client.CharacterInfo = matchingData.CharacterInfo;
if (matchingData != null) { client.CharacterInfo = matchingData.CharacterInfo; }
}
}
@@ -166,10 +349,49 @@ namespace Barotrauma
return assignedJobs;
}
public override void Update(float deltaTime)
{
if (CoroutineManager.IsCoroutineRunning("LevelTransition")) { return; }
base.Update(deltaTime);
if (Level.Loaded != null)
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
var transitionType = GetAvailableTransition(out _, out Submarine leavingSub);
if (transitionType == TransitionType.End)
{
LoadNewLevel();
}
else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub))
{
LoadNewLevel();
}
else if (transitionType == TransitionType.ReturnToPreviousLocation && Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub))
{
LoadNewLevel();
}
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
KeepCharactersCloseToOutpost(deltaTime);
}
}
}
public override void End(TransitionType transitionType = TransitionType.None)
{
GameOver = !GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
base.End(transitionType);
}
public void ServerWrite(IWriteMessage msg, Client c)
{
System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue);
Reputation reputation = Map?.CurrentLocation?.Reputation;
msg.Write(IsFirstRound);
msg.Write(CampaignID);
msg.Write(lastUpdateID);
msg.Write(lastSaveID);
@@ -177,15 +399,53 @@ namespace Barotrauma
msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex);
msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex);
msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex);
msg.Write(reputation != null);
if (reputation != null) { msg.Write(reputation.Value); }
msg.Write(isRunning && startWatchman != null ? startWatchman.ID : (UInt16)0);
msg.Write(isRunning && endWatchman != null ? endWatchman.ID : (UInt16)0);
// hopefully we'll never have more than 128 factions
msg.Write((byte)Factions.Count);
foreach (Faction faction in Factions)
{
msg.Write(faction.Prefab.Identifier);
msg.Write(faction.Reputation.Value);
}
msg.Write(ForceMapUI);
msg.Write(Money);
msg.Write(PurchasedHullRepairs);
msg.Write(PurchasedItemRepairs);
msg.Write(PurchasedLostShuttles);
if (map.CurrentLocation != null)
{
msg.Write((byte)map.CurrentLocation?.AvailableMissions.Count());
foreach (Mission mission in map.CurrentLocation.AvailableMissions)
{
msg.Write(mission.Prefab.Identifier);
Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0];
LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination);
msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection));
}
// Store balance
msg.Write(true);
msg.Write((UInt16)map.CurrentLocation.StoreCurrentBalance);
}
else
{
msg.Write((byte)0);
// Store balance
msg.Write(false);
}
msg.Write((UInt16)CargoManager.ItemsInBuyCrate.Count);
foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate)
{
msg.Write(pi.ItemPrefab.Identifier);
msg.WriteRangedInteger(pi.Quantity, 0, 100);
}
msg.Write((UInt16)CargoManager.PurchasedItems.Count);
foreach (PurchasedItem pi in CargoManager.PurchasedItems)
{
@@ -193,6 +453,23 @@ namespace Barotrauma
msg.WriteRangedInteger(pi.Quantity, 0, 100);
}
msg.Write((UInt16)CargoManager.SoldItems.Count);
foreach (SoldItem si in CargoManager.SoldItems)
{
msg.Write(si.ItemPrefab.Identifier);
msg.Write((UInt16)si.ID);
msg.Write(si.Removed);
msg.Write(si.SellerID);
}
msg.Write((ushort)UpgradeManager.PendingUpgrades.Count);
foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades)
{
msg.Write(prefab.Identifier);
msg.Write(category.Identifier);
msg.Write((byte)level);
}
var characterData = GetClientCharacterData(c);
if (characterData?.CharacterInfo == null)
{
@@ -207,13 +484,23 @@ namespace Barotrauma
public void ServerRead(IReadMessage msg, Client sender)
{
UInt16 currentLocIndex = msg.ReadUInt16();
UInt16 selectedLocIndex = msg.ReadUInt16();
byte selectedMissionIndex = msg.ReadByte();
bool purchasedHullRepairs = msg.ReadBoolean();
bool purchasedItemRepairs = msg.ReadBoolean();
bool purchasedLostShuttles = msg.ReadBoolean();
UInt16 purchasedItemCount = msg.ReadUInt16();
UInt16 buyCrateItemCount = msg.ReadUInt16();
List<PurchasedItem> buyCrateItems = new List<PurchasedItem>();
for (int i = 0; i < buyCrateItemCount; i++)
{
string itemPrefabIdentifier = msg.ReadString();
int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity);
buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity));
}
UInt16 purchasedItemCount = msg.ReadUInt16();
List<PurchasedItem> purchasedItems = new List<PurchasedItem>();
for (int i = 0; i < purchasedItemCount; i++)
{
@@ -222,36 +509,68 @@ namespace Barotrauma
purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity));
}
if (!sender.HasPermission(ClientPermissions.ManageCampaign))
UInt16 soldItemCount = msg.ReadUInt16();
List<SoldItem> soldItems = new List<SoldItem>();
for (int i = 0; i < soldItemCount; i++)
{
string itemPrefabIdentifier = msg.ReadString();
UInt16 id = msg.ReadUInt16();
bool removed = msg.ReadBoolean();
byte sellerId = msg.ReadByte();
soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId));
}
ushort purchasedUpgradeCount = msg.ReadUInt16();
List<PurchasedUpgrade> purchasedUpgrades = new List<PurchasedUpgrade>();
for (int i = 0; i < purchasedUpgradeCount; i++)
{
string upgradeIdentifier = msg.ReadString();
UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier);
string categoryIdentifier = msg.ReadString();
UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier);
int upgradeLevel = msg.ReadByte();
if (category == null || prefab == null) { continue; }
purchasedUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel));
}
if (!AllowedToManageCampaign(sender))
{
DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign");
return;
}
Location location = Map.CurrentLocation;
int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost;
int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost;
int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost;
if (purchasedHullRepairs != this.PurchasedHullRepairs)
{
if (purchasedHullRepairs && Money >= HullRepairCost)
if (purchasedHullRepairs && Money >= hullRepairCost)
{
this.PurchasedHullRepairs = true;
Money -= HullRepairCost;
Money -= hullRepairCost;
}
else if (!purchasedHullRepairs)
{
this.PurchasedHullRepairs = false;
Money += HullRepairCost;
Money += hullRepairCost;
}
}
if (purchasedItemRepairs != this.PurchasedItemRepairs)
{
if (purchasedItemRepairs && Money >= ItemRepairCost)
if (purchasedItemRepairs && Money >= itemRepairCost)
{
this.PurchasedItemRepairs = true;
Money -= ItemRepairCost;
Money -= itemRepairCost;
}
else if (!purchasedItemRepairs)
{
this.PurchasedItemRepairs = false;
Money += ItemRepairCost;
Money += itemRepairCost;
}
}
if (purchasedLostShuttles != this.PurchasedLostShuttles)
@@ -261,41 +580,196 @@ namespace Barotrauma
{
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox);
}
else if (purchasedLostShuttles && Money >= ShuttleReplaceCost)
else if (purchasedLostShuttles && Money >= shuttleRetrieveCost)
{
this.PurchasedLostShuttles = true;
Money -= ShuttleReplaceCost;
Money -= shuttleRetrieveCost;
}
else if (!purchasedItemRepairs)
{
this.PurchasedLostShuttles = false;
Money += ShuttleReplaceCost;
Money += shuttleRetrieveCost;
}
}
#if DEBUG
if (currentLocIndex < Map.Locations.Count)
{
Map.SetLocation(currentLocIndex);
}
#endif
Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex);
if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); }
if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndex); }
List<PurchasedItem> currentItems = new List<PurchasedItem>(CargoManager.PurchasedItems);
foreach (PurchasedItem pi in currentItems)
List<PurchasedItem> currentBuyCrateItems = new List<PurchasedItem>(CargoManager.ItemsInBuyCrate);
currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity));
buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity));
CargoManager.SellBackPurchasedItems(new List<PurchasedItem>(CargoManager.PurchasedItems));
CargoManager.PurchaseItems(purchasedItems, false);
// for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all
// sold items that are removed so they should be discarded on the next message
CargoManager.BuyBackSoldItems(new List<SoldItem>(CargoManager.SoldItems));
CargoManager.SellItems(soldItems);
foreach (var (prefab, category, _) in purchasedUpgrades)
{
CargoManager.SellItem(pi, pi.Quantity);
UpgradeManager.PurchaseUpgrade(prefab, category);
// unstable logging
int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation);
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
}
}
public void ServerReadCrew(IReadMessage msg, Client sender)
{
int[] pendingHires = null;
bool updatePending = msg.ReadBoolean();
if (updatePending)
{
ushort pendingHireLength = msg.ReadUInt16();
pendingHires = new int[pendingHireLength];
for (int i = 0; i < pendingHireLength; i++)
{
pendingHires[i] = msg.ReadInt32();
}
}
foreach (PurchasedItem pi in purchasedItems)
bool validateHires = msg.ReadBoolean();
bool fireCharacter = msg.ReadBoolean();
int firedIdentifier = -1;
if (fireCharacter) { firedIdentifier = msg.ReadInt32(); }
Location location = map?.CurrentLocation;
CharacterInfo firedCharacter = null;
if (location != null && AllowedToManageCampaign(sender))
{
CargoManager.PurchaseItem(pi.ItemPrefab, pi.Quantity);
if (fireCharacter)
{
firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier);
if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true))
{
CrewManager.FireCharacter(firedCharacter);
}
else
{
DebugConsole.ThrowError($"Tried to fire an invalid character ({firedIdentifier})");
}
}
if (location.HireManager != null)
{
if (validateHires)
{
foreach (CharacterInfo hireInfo in location.HireManager.PendingHires)
{
TryHireCharacter(location, hireInfo);
}
}
if (updatePending)
{
List<CharacterInfo> pendingHireInfos = new List<CharacterInfo>();
foreach (int identifier in pendingHires)
{
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifier() == identifier);
if (match == null)
{
DebugConsole.ThrowError($"Tried to hire a character that doesn't exist ({identifier})");
continue;
}
pendingHireInfos.Add(match);
}
location.HireManager.PendingHires = pendingHireInfos;
}
}
}
// bounce back
SendCrewState(validateHires, firedCharacter);
}
/// <summary>
/// Notifies the clients of the current bot situation like syncing pending and available hires
/// available hires are also synced
/// </summary>
/// <param name="validateHires">When set to true notifies the clients that the hires have been validated.</param>
/// <param name="firedCharacter">When not null will inform the clients that his character has been fired.</param>
/// <remarks>
/// It might be obsolete to sync available hires. I found that the available hires are always the same between
/// the client and the server when there's only one person on the server but when a second person joins both of
/// their available hires are different from the server.
/// </remarks>
public void SendCrewState(bool validateHires, CharacterInfo firedCharacter)
{
List<CharacterInfo> availableHires = new List<CharacterInfo>();
List<CharacterInfo> pendingHires = new List<CharacterInfo>();
if (map.CurrentLocation != null && map.CurrentLocation.Type.HasHireableCharacters)
{
availableHires = map.CurrentLocation.GetHireableCharacters().ToList();
pendingHires = map.CurrentLocation?.HireManager.PendingHires;
}
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage msg = new WriteOnlyMessage();
msg.Write((byte)ServerPacketHeader.CREW);
msg.Write((ushort)availableHires.Count);
foreach (CharacterInfo hire in availableHires)
{
hire.ServerWrite(msg);
msg.Write(hire.Salary);
}
msg.Write((ushort)pendingHires.Count);
foreach (CharacterInfo pendingHire in pendingHires)
{
msg.Write(pendingHire.GetIdentifier());
}
msg.Write(validateHires);
msg.Write(firedCharacter != null);
if (firedCharacter != null) { msg.Write(firedCharacter.GetIdentifier()); }
GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
}
}
public override void Save(XElement element)
{
element.Add(new XAttribute("campaignid", CampaignID));
XElement modeElement = new XElement("MultiPlayerCampaign",
new XAttribute("money", Money),
new XAttribute("cheatsenabled", CheatsEnabled),
new XAttribute("initialsuppliesspawned", InitialSuppliesSpawned));
new XAttribute("cheatsenabled", CheatsEnabled));
CampaignMetadata?.Save(modeElement);
Map.Save(modeElement);
CargoManager?.SavePurchasedItems(modeElement);
UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades);
// save bots
CrewManager.SaveMultiplayer(modeElement);
// save available submarines
XElement availableSubsElement = new XElement("AvailableSubs");
for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++)
{
availableSubsElement.Add(new XElement("Sub", new XAttribute("name", GameMain.NetLobbyScreen.CampaignSubmarines[i].Name)));
}
modeElement.Add(availableSubsElement);
element.Add(modeElement);
//save character data to a separate file
@@ -0,0 +1,52 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class UpgradeManager
{
partial void UpgradeNPCSpeak(string text, bool isSinglePlayer, Character? character)
{
if (Level.Loaded?.StartOutpost?.Info?.OutpostNPCs == null) { return; }
foreach (Character npc in Level.Loaded.StartOutpost.Info.OutpostNPCs.SelectMany(kpv => kpv.Value))
{
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Upgrade)
{
npc.Speak(text, ChatMessageType.Default);
break;
}
}
}
/// <summary>
/// Sends a message to all clients telling them that all upgrades on the submarine were reset.
/// </summary>
/// <remarks>
/// <param name="newUpgrades"/> is supposed to have a list of reloaded metadata but seeing as
/// this method is currently only used when switching submarines and that disables the repair NPC
/// until the next round so currently there's no need for it as we get the new values from the save
/// file anyways.
/// </remarks>
/// <see cref="UpgradeManager.ClientRead"/>
private void SendUpgradeResetMessage(Dictionary<string, int> newUpgrades)
{
foreach (Client c in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte)ServerPacketHeader.RESET_UPGRADES);
outmsg.Write(true);
outmsg.Write(Campaign.Money);
// outmsg.Write((uint)newUpgrades.Count);
// foreach (var (key, value) in newUpgrades)
// {
// outmsg.Write(key);
// outmsg.Write((byte)value);
// }
GameMain.Server?.ServerPeer?.Send(outmsg, c.Connection, DeliveryMethod.Reliable);
}
}
}
}
@@ -5,21 +5,21 @@ namespace Barotrauma.Items.Components
{
partial class ItemLabel : ItemComponent, IDrawableComponent
{
[Serialize("", true, description: "The text to display on the label."), Editable(100)]
[Serialize("", true, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)]
public string Text
{
get;
set;
}
[Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.")]
[Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)]
public Color TextColor
{
get;
set;
}
[Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.")]
[Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)]
public float TextScale
{
get;
@@ -0,0 +1,17 @@
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class OutpostTerminal : ItemComponent, IClientSerializable, IServerSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
}
}
}
@@ -136,6 +136,7 @@ namespace Barotrauma
{
if (Owner == c.Character)
{
HumanAIController.ItemTaken(item, c.Character);
GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory);
}
else
@@ -138,6 +138,32 @@ namespace Barotrauma
errorMsg = "Failed to write a ChangeProperty network event for the item \"" + Name + "\" (" + e.Message + ")";
}
break;
case NetEntityEvent.Type.Upgrade:
{
if (extraData.Length > 0 && extraData[1] is Upgrade upgrade)
{
var upgradeTargets = upgrade.TargetComponents;
msg.Write(upgrade.Identifier);
msg.Write((byte)upgrade.Level);
msg.Write((byte)upgradeTargets.Count);
foreach (var (_, value) in upgrade.TargetComponents)
{
msg.Write((byte)value.Length);
foreach (var propertyReference in value)
{
object originalValue = propertyReference.OriginalValue;
msg.Write((float)(originalValue ?? -1));
}
}
}
else
{
errorMsg = extraData.Length > 0
? $"Failed to write a network event for the item \"{Name}\" - \"{extraData[1].GetType()}\" is not a valid upgrade."
: $"Failed to write a network event for the item \"{Name}\". No upgrade specified.";
}
break;
}
default:
errorMsg = "Failed to write a network event for the item \"" + Name + "\" - \"" + eventType + "\" is not a valid entity event type for items.";
break;
@@ -152,7 +178,6 @@ namespace Barotrauma
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
}
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
@@ -213,7 +238,7 @@ namespace Barotrauma
}
}
public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID)
public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex)
{
if (GameMain.Server == null) return;
@@ -238,29 +263,14 @@ namespace Barotrauma
else
{
msg.Write(originalInventoryID);
//find the index of the ItemContainer this item is inside to get the item to
//spawn in the correct inventory in multi-inventory items like fabricators
byte containerIndex = 0;
if (Container != null)
{
for (int i = 0; i < Container.components.Count; i++)
{
if (Container.components[i] is ItemContainer container &&
container.Inventory == ParentInventory)
{
containerIndex = (byte)i;
break;
}
}
}
msg.Write(containerIndex);
msg.Write(originalItemContainerIndex);
int slotIndex = ParentInventory.FindIndex(this);
msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex);
}
msg.Write(body == null ? (byte)0 : (byte)body.BodyType);
msg.Write(SpawnedInOutpost);
byte teamID = 0;
foreach (WifiComponent wifiComponent in GetComponents<WifiComponent>())
@@ -14,7 +14,7 @@ namespace Barotrauma
msg.Write((byte)Sections.Length);
for (int i = 0; i < Sections.Length; i++)
{
msg.WriteRangedSingle(Sections[i].damage / Health, 0.0f, 1.0f, 8);
msg.WriteRangedSingle(Sections[i].damage / MaxHealth, 0.0f, 1.0f, 8);
}
}
}
@@ -30,7 +30,8 @@ namespace Barotrauma.Networking
if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count)
{
DebugConsole.ThrowError("Invalid order message from client \"" + c.Name + "\" - order index out of bounds.");
DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex}, {orderOptionIndex}).");
if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; }
return;
}
@@ -44,7 +45,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString() ?? "";
}
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) return;
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }
c.LastSentChatMsgID = ID;
@@ -168,6 +169,10 @@ namespace Barotrauma.Networking
{
msg.Write(Sender.ID);
}
if (Type == ChatMessageType.ServerMessageBoxInGame)
{
msg.Write(IconStyle);
}
}
}
}
@@ -35,7 +35,7 @@ namespace Barotrauma
{
message.Write((byte)SpawnableType.Item);
DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")");
((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID);
((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex);
}
else if (entities.Entity is Character)
{
@@ -11,12 +11,6 @@ namespace Barotrauma.Networking
{
public class FileTransferOut
{
private readonly byte[] data;
private readonly DateTime startingTime;
private readonly NetworkConnection connection;
public FileTransferStatus Status;
public string FileName
@@ -48,10 +42,7 @@ namespace Barotrauma.Networking
set;
}
public byte[] Data
{
get { return data; }
}
public byte[] Data { get; }
public bool Acknowledged;
@@ -63,16 +54,15 @@ namespace Barotrauma.Networking
public int KnownReceivedOffset;
public NetworkConnection Connection
{
get { return connection; }
}
public NetworkConnection Connection { get; }
public DateTime StartingTime { get; }
public int ID;
public FileTransferOut(NetworkConnection recipient, FileTransferType fileType, string filePath)
{
connection = recipient;
Connection = recipient;
FileType = fileType;
FilePath = filePath;
@@ -84,14 +74,14 @@ namespace Barotrauma.Networking
Status = FileTransferStatus.NotStarted;
startingTime = DateTime.Now;
StartingTime = DateTime.Now;
int maxRetries = 4;
for (int i = 0; i <= maxRetries; i++)
{
try
{
data = File.ReadAllBytes(filePath);
Data = File.ReadAllBytes(filePath);
}
catch (System.IO.IOException e)
{
@@ -192,97 +182,104 @@ namespace Barotrauma.Networking
foreach (FileTransferOut transfer in activeTransfers)
{
transfer.WaitTimer -= deltaTime;
if (transfer.WaitTimer > 0.0f) continue;
transfer.WaitTimer = 0.05f;// transfer.Connection.AverageRoundtripTime;
// send another part of the file
long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining);
IWriteMessage message;
try
for (int i = 0; i < 10; i++)
{
//first message; send length, file name etc
//wait for acknowledgement before sending data
if (!transfer.Acknowledged)
{
message = new WriteOnlyMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
if (transfer.WaitTimer > 0.0f) { break; }
Send(transfer);
}
}
}
//if the recipient is the owner of the server (= a client running the server from the main exe)
//we don't need to send anything, the client can just read the file directly
if (transfer.Connection == GameMain.Server.OwnerConnection)
{
message.Write((byte)FileTransferMessageType.TransferOnSameMachine);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
message.Write(transfer.FilePath);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
transfer.Status = FileTransferStatus.Finished;
}
else
{
message.Write((byte)FileTransferMessageType.Initiate);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
//message.Write((ushort)chunkLen);
message.Write(transfer.Data.Length);
message.Write(transfer.FileName);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
private void Send(FileTransferOut transfer)
{
// send another part of the file
long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining);
transfer.Status = FileTransferStatus.Sending;
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending file transfer initiation message: ");
DebugConsole.Log(" File: " + transfer.FileName);
DebugConsole.Log(" Size: " + transfer.Data.Length);
DebugConsole.Log(" ID: " + transfer.ID);
}
}
return;
}
IWriteMessage message;
try
{
//first message; send length, file name etc
//wait for acknowledgement before sending data
if (!transfer.Acknowledged)
{
message = new WriteOnlyMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
message.Write((byte)FileTransferMessageType.Data);
message.Write((byte)transfer.ID);
message.Write(transfer.SentOffset);
byte[] sendBytes = new byte[sendByteCount];
Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount);
message.Write((ushort)sendByteCount);
message.Write(sendBytes, 0, sendByteCount);
transfer.SentOffset += sendByteCount;
if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 5 ||
transfer.SentOffset >= transfer.Data.Length)
//if the recipient is the owner of the server (= a client running the server from the main exe)
//we don't need to send anything, the client can just read the file directly
if (transfer.Connection == GameMain.Server.OwnerConnection)
{
transfer.SentOffset = transfer.KnownReceivedOffset;
message.Write((byte)FileTransferMessageType.TransferOnSameMachine);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
message.Write(transfer.FilePath);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
transfer.Status = FileTransferStatus.Finished;
}
else
{
message.Write((byte)FileTransferMessageType.Initiate);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
//message.Write((ushort)chunkLen);
message.Write(transfer.Data.Length);
message.Write(transfer.FileName);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
transfer.Status = FileTransferStatus.Sending;
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending file transfer initiation message: ");
DebugConsole.Log(" File: " + transfer.FileName);
DebugConsole.Log(" Size: " + transfer.Data.Length);
DebugConsole.Log(" ID: " + transfer.ID);
}
}
transfer.WaitTimer = 0.1f;
return;
}
catch (Exception e)
message = new WriteOnlyMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
message.Write((byte)FileTransferMessageType.Data);
message.Write((byte)transfer.ID);
message.Write(transfer.SentOffset);
byte[] sendBytes = new byte[sendByteCount];
Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount);
message.Write((ushort)sendByteCount);
message.Write(sendBytes, 0, sendByteCount);
transfer.SentOffset += sendByteCount;
if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 10 ||
transfer.SentOffset >= transfer.Data.Length)
{
DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e);
GameAnalyticsManager.AddErrorEventOnce(
"FileSender.Update:Exception",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace);
transfer.Status = FileTransferStatus.Error;
break;
transfer.SentOffset = transfer.KnownReceivedOffset;
transfer.WaitTimer = 0.5f;
}
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending " + sendByteCount + " bytes of the file " + transfer.FileName + " (" + transfer.SentOffset + "/" + transfer.Data.Length + " sent)");
}
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
}
catch (Exception e)
{
DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e);
GameAnalyticsManager.AddErrorEventOnce(
"FileSender.Update:Exception",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace);
transfer.Status = FileTransferStatus.Error;
return;
}
if (GameSettings.VerboseLogging)
{
DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)");
}
}
@@ -317,7 +314,11 @@ namespace Barotrauma.Networking
matchingTransfer.Acknowledged = true;
int offset = inc.ReadInt32();
matchingTransfer.KnownReceivedOffset = offset > matchingTransfer.KnownReceivedOffset ? offset : matchingTransfer.KnownReceivedOffset;
if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset) { matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; }
if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset)
{
matchingTransfer.WaitTimer = 0.0f;
matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset;
}
if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
{
File diff suppressed because it is too large Load Diff
@@ -255,8 +255,8 @@ namespace Barotrauma.Networking
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected old event "
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event ("
+ (c.LastRecvEntityEventID + 1).ToString() +
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
@@ -273,8 +273,8 @@ namespace Barotrauma.Networking
List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent");
});
}
@@ -11,6 +11,7 @@ namespace Barotrauma.Networking
partial class ServerSettings
{
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml";
public static readonly char SubmarineSeparatorChar = '|';
partial void InitProjSpecific()
{
@@ -16,6 +16,51 @@ namespace Barotrauma
{
get { return allowModeVoting; }
set { allowModeVoting = value; }
}
public struct SubmarineVote
{
public Client VoteStarter;
public SubmarineInfo Sub;
public VoteType VoteType;
public float Timer;
public int DeliveryFee;
public VoteState State;
}
public static SubmarineVote SubVote;
private void StartSubmarineVote(IReadMessage inc, VoteType voteType, Client sender)
{
string subName = inc.ReadString();
SubVote.Sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
SubVote.DeliveryFee = voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0;
SubVote.VoteType = voteType;
SubVote.State = VoteState.Started;
SubVote.VoteStarter = sender;
VoteRunning = true;
sender.SetVote(voteType, 2);
}
public void StopSubmarineVote(bool passed)
{
VoteRunning = false;
SubVote.State = passed ? VoteState.Passed : VoteState.Failed;
GameMain.Server.UpdateVoteStatus();
GameMain.NetworkMember.SubmarineVoteYesCount = GameMain.NetworkMember.SubmarineVoteNoCount = GameMain.NetworkMember.SubmarineVoteMax = 0;
for (int i = 0; i < GameMain.NetworkMember.ConnectedClients.Count; i++)
{
GameMain.NetworkMember.ConnectedClients[i].SetVote(SubVote.VoteType, 0);
}
SubVote.Sub = null;
SubVote.DeliveryFee = 0;
SubVote.VoteType = VoteType.Unknown;
SubVote.Timer = 0.0f;
SubVote.State = VoteState.None;
SubVote.VoteStarter = null;
}
public void ServerRead(IReadMessage inc, Client sender)
@@ -76,7 +121,24 @@ namespace Barotrauma
sender.SetVote(VoteType.StartRound, ready);
GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage);
}
break;
case VoteType.PurchaseAndSwitchSub:
case VoteType.PurchaseSub:
case VoteType.SwitchSub:
bool startVote = inc.ReadBoolean();
if (startVote)
{
StartSubmarineVote(inc, voteType, sender);
}
else
{
sender.SetVote(voteType, (int)inc.ReadByte());
}
GameMain.Server.SubmarineVoteYesCount = GameMain.Server.ConnectedClients.Count(c => c.GetVote<int>(SubVote.VoteType) == 2);
GameMain.Server.SubmarineVoteNoCount = GameMain.Server.ConnectedClients.Count(c => c.GetVote<int>(SubVote.VoteType) == 1);
GameMain.Server.SubmarineVoteMax = GameMain.Server.ConnectedClients.Count(c => c.InGame);
break;
}
@@ -120,6 +182,52 @@ namespace Barotrauma
msg.Write(AllowVoteKick);
msg.Write((byte)SubVote.State);
if (SubVote.State != VoteState.None)
{
msg.Write((byte)SubVote.VoteType);
if (SubVote.VoteType != VoteType.Unknown)
{
var yesClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote<int>(SubVote.VoteType) == 2);
msg.Write((byte)yesClients.Count);
foreach (Client c in yesClients)
{
msg.Write(c.ID);
}
var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote<int>(SubVote.VoteType) == 1);
msg.Write((byte)noClients.Count);
foreach (Client c in noClients)
{
msg.Write(c.ID);
}
msg.Write((byte)GameMain.Server.SubmarineVoteMax);
switch (SubVote.State)
{
case VoteState.Started:
msg.Write(SubVote.Sub.Name);
msg.Write(SubVote.VoteStarter.ID);
msg.Write((byte)GameMain.Server.ServerSettings.SubmarineVoteTimeout);
break;
case VoteState.Running:
// Nothing specific
break;
case VoteState.Passed:
case VoteState.Failed:
msg.Write(SubVote.State == VoteState.Passed);
msg.Write(SubVote.Sub.Name);
if (SubVote.State == VoteState.Passed)
{
msg.Write((short)SubVote.DeliveryFee);
}
break;
}
}
}
var readyClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote<bool>(VoteType.StartRound));
msg.Write((byte)readyClients.Count);
foreach (Client c in readyClients)
@@ -69,12 +69,19 @@ namespace Barotrauma
static void CrashDump(string filePath, Exception exception)
{
GameMain.Server?.ServerSettings?.SaveSettings();
GameMain.Server?.ServerSettings?.BanList.Save();
if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom")
try
{
GameMain.Server?.KarmaManager?.SaveCustomPreset();
GameMain.Server?.KarmaManager?.Save();
GameMain.Server?.ServerSettings?.SaveSettings();
GameMain.Server?.ServerSettings?.BanList.Save();
if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom")
{
GameMain.Server?.KarmaManager?.SaveCustomPreset();
GameMain.Server?.KarmaManager?.Save();
}
}
catch (Exception e)
{
//couldn't save, whatever
}
int existingFiles = 0;
@@ -146,11 +153,11 @@ namespace Barotrauma
{
GameAnalytics.AddErrorEvent(EGAErrorSeverity.Critical, crashReport);
GameAnalytics.OnQuit();
Console.Write("A crash report (\"crashreport.log\") was saved in the root folder of the game and sent to the developers.");
Console.Write("A crash report (\"servercrashreport.log\") was saved in the root folder of the game and sent to the developers.");
}
else
{
Console.Write("A crash report(\"crashreport.log\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" +
Console.Write("A crash report(\"servercrashreport.log\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" +
" if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/");
}
SteamManager.ShutDown();
@@ -30,6 +30,61 @@ namespace Barotrauma
set { selectedShuttle = value; lastUpdateID++; }
}
public List<SubmarineInfo> CampaignSubmarines
{
get
{
return campaignSubmarines;
}
set
{
campaignSubmarines = value;
lastUpdateID++;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
}
private List<SubmarineInfo> campaignSubmarines;
public void AddCampaignSubmarine(SubmarineInfo sub)
{
if (!campaignSubmarines.Contains(sub))
{
campaignSubmarines.Add(sub);
}
else
{
return;
}
lastUpdateID++;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
public void RemoveCampaignSubmarine(SubmarineInfo sub)
{
if (campaignSubmarines.Contains(sub))
{
campaignSubmarines.Remove(sub);
}
else
{
return;
}
lastUpdateID++;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
public GameModePreset[] GameModes { get; }
private int selectedModeIndex;
@@ -40,6 +95,10 @@ namespace Barotrauma
{
lastUpdateID++;
selectedModeIndex = MathHelper.Clamp(value, 0, GameModes.Length - 1);
if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this)
{
GameMain.GameSession = null;
}
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.GameModeIdentifier = SelectedModeIdentifier;
@@ -121,7 +180,7 @@ namespace Barotrauma
{
LevelSeed = ToolBox.RandomSeed(8);
subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList();
subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)).ToList();
if (subs == null || subs.Count() == 0)
{
@@ -176,7 +235,7 @@ namespace Barotrauma
{
for (int i = 0; i < GameModes.Length; i++)
{
if ((GameModes[i].Identifier == "multiplayercampaign") == enabled)
if ((GameModes[i] == GameModePreset.MultiPlayerCampaign) == enabled)
{
selectedModeIndex = i;
break;
@@ -190,6 +249,10 @@ namespace Barotrauma
{
base.Select();
GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients);
if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this)
{
GameMain.GameSession = null;
}
}
public void RandomizeSettings()
@@ -203,7 +266,7 @@ namespace Barotrauma
}
if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random)
{
var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m.Identifier != "multiplayercampaign");
var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign);
SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier;
}
}
@@ -9,7 +9,7 @@ using System.Linq;
namespace Barotrauma
{
partial class TraitorManager
partial class TraitorManager
{
public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks);
@@ -182,14 +182,21 @@ namespace Barotrauma
}
}
public string GetEndMessage()
public List<TraitorMissionResult> GetEndResults()
{
#if DISABLE_MISSIONS
return "";
#endif
if (GameMain.Server == null || !Missions.Any()) return "";
List<TraitorMissionResult> results = new List<TraitorMissionResult>();
return TextManager.JoinServerMessages("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage).ToArray());
#if DISABLE_MISSIONS
return results;
#endif
if (GameMain.Server == null || !Missions.Any()) { return results; }
foreach (var mission in Missions)
{
results.Add(new TraitorMissionResult(mission.Value));
}
return results;
}
}
}
@@ -0,0 +1,30 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class TraitorMissionResult
{
public TraitorMissionResult(Traitor.TraitorMission mission)
{
MissionIdentifier = mission.Identifier;
EndMessage = mission.GlobalEndMessage;
Success = mission.IsCompleted;
foreach (Traitor traitor in mission.Traitors.Values)
{
Characters.Add(traitor.Character);
}
}
public void ServerWrite(IWriteMessage msg)
{
msg.Write(MissionIdentifier);
msg.Write(EndMessage);
msg.Write(Success);
msg.Write((byte)Characters.Count);
foreach (Character character in Characters)
{
msg.Write(character.ID);
}
}
}
}