Unstable 1.1.14.0

This commit is contained in:
Markus Isberg
2023-10-02 16:43:54 +03:00
parent 94f5a93a0c
commit cf8f0de659
606 changed files with 21906 additions and 11456 deletions
@@ -376,10 +376,9 @@ namespace Barotrauma
tempBuffer.WriteBoolean(aiming);
tempBuffer.WriteBoolean(shoot);
tempBuffer.WriteBoolean(use);
if (AnimController is HumanoidAnimController)
{
tempBuffer.WriteBoolean(((HumanoidAnimController)AnimController).Crouching);
}
tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true });
tempBuffer.WriteBoolean(attack);
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
@@ -430,21 +429,31 @@ namespace Barotrauma
if (writeStatus)
{
WriteStatus(tempBuffer);
AIController?.ServerWrite(tempBuffer);
tempBuffer.WriteBoolean(AIController is EnemyAIController);
if (AIController is EnemyAIController enemyAi)
{
tempBuffer.WriteByte((byte)enemyAi.State);
tempBuffer.WriteBoolean(enemyAi.PetBehavior is PetBehavior);
if (enemyAi.PetBehavior is PetBehavior petBehavior)
{
tempBuffer.WriteByte((byte)((petBehavior.Happiness / petBehavior.MaxHappiness) * byte.MaxValue));
tempBuffer.WriteByte((byte)((petBehavior.Hunger / petBehavior.MaxHunger) * byte.MaxValue));
}
}
HealthUpdatePending = false;
}
}
public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); }
if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); }
msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue);
switch (eventData)
{
case InventoryStateEventData _:
case InventoryStateEventData inventoryData:
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
Inventory.ServerEventWrite(msg, c);
Inventory.ServerEventWrite(msg, c, inventoryData);
break;
case ControlEventData controlEventData:
Client owner = controlEventData.Owner;
@@ -473,12 +482,12 @@ namespace Barotrauma
case IAttackEventData attackEventData:
{
int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb);
ushort targetEntityId = 0;
ushort targetEntityId = NullEntityID;
int targetLimbIndex = -1;
if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity)
{
targetEntityId = targetEntity.ID;
if (targetEntity is Character { AnimController: { Limbs: var targetLimbsArray } })
if (targetEntity is Character { AnimController.Limbs: var targetLimbsArray })
{
targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb);
}
@@ -0,0 +1,16 @@
#nullable enable
using Barotrauma.Items.Components;
namespace Barotrauma
{
internal abstract partial class CircuitBoxConnection
{
public string Name => Connection.Name;
private partial void InitProjSpecific(CircuitBox circuitBox)
{
Length = 100f;
}
}
}
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Text;
using Barotrauma.Steam;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -1020,11 +1021,6 @@ namespace Barotrauma
client.SpectateOnly = false;
});
AssignOnExecute("starttraitormissionimmediately", (string[] args) =>
{
GameMain.Server?.TraitorManager?.SkipStartDelay();
});
AssignOnExecute("difficulty|leveldifficulty", (string[] args) =>
{
if (GameMain.Server == null || args.Length < 1) return;
@@ -1082,56 +1078,83 @@ namespace Barotrauma
GameMain.Server?.UpdateCheatsEnabled();
});
commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their targets.", (string[] args) =>
AssignOnExecute("triggertraitorevent", (string[] args) =>
{
if (GameMain.Server == null) return;
TraitorManager traitorManager = GameMain.Server.TraitorManager;
if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any())
if (GameMain.Server?.TraitorManager == null)
{
NewMessage("There are no traitors at the moment.", Color.Cyan);
ThrowError($"Could not start a traitor event. {nameof(TraitorManager)} hasn't been created.");
return;
}
foreach (Traitor t in traitorManager.Traitors)
if (args.Length > 0)
{
if (t.CurrentObjective != null)
Identifier traitorEventId = args[0].ToIdentifier();
if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab prefab) && prefab is TraitorEventPrefab traitorEventPrefab)
{
NewMessage(string.Format("- Traitor {0}'s current goals are:\n{1}", t.Character.Name, t.CurrentObjective.GoalInfos), Color.Cyan);
GameMain.Server?.TraitorManager?.ForceTraitorEvent(traitorEventPrefab);
}
else
{
NewMessage(string.Format("- Traitor {0} has no current objective.", t.Character.Name), Color.Cyan);
ThrowError($"Could not find a traitor event prefab with the identifier \"{traitorEventId}\".");
return;
}
}
//NewMessage("The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", Color.Cyan);
if (GameMain.Server?.TraitorManager is { } traitorManager)
{
traitorManager.Enabled = true;
traitorManager.SkipStartDelay();
}
});
commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their current objectives.", (string[] args) =>
{
if (GameMain.Server == null) { return; }
CreateTraitorList((string msg) => NewMessage(msg, Color.Cyan));
}));
AssignOnClientRequestExecute("traitorlist", (Client client, Vector2 cursorPos, string[] args) =>
{
TraitorManager traitorManager = GameMain.Server.TraitorManager;
if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any())
if (GameMain.Server == null) { return; }
CreateTraitorList((string msg) =>
{
GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", Identifier.Empty, TraitorMessageType.Console);
GameMain.Server.SendDirectChatMessage(msg, client);
});
});
void CreateTraitorList(Action<string> createMessage)
{
if (GameMain.Server == null) return;
TraitorManager traitorManager = GameMain.Server.TraitorManager;
if (traitorManager == null || traitorManager.ActiveEvents.None())
{
createMessage("There are no traitors at the moment.");
return;
}
foreach (Traitor t in traitorManager.Traitors)
createMessage("Traitors:");
foreach (var ev in traitorManager.ActiveEvents)
{
if (t.CurrentObjective != null)
createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})");
}
}
AssignOnClientRequestExecute(
"debugevent",
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (GameMain.Server == null) { return; }
if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0)
{
var traitorGoals = TextManager.FormatServerMessage(t.CurrentObjective.GoalInfos);
var traitorGoalsStart = traitorGoals.LastIndexOf('/') + 1;
GameMain.Server.SendTraitorMessage(client, string.Join("/", new[] {
traitorGoals.Substring(0, traitorGoalsStart),
$"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}",
$"[traitorname]={t.Character.Name}",
"Traitor [traitorname]'s current goals are:\n[traitorgoals]"
}.Where(s => !string.IsNullOrEmpty(s))), t.Mission.Identifier, TraitorMessageType.Console);
}
else
{
GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", t.Character.Name), Identifier.Empty, TraitorMessageType.Console);
var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
if (ev == null)
{
GameMain.Server.SendConsoleMessage($"Event \"{args[0]}\" not found.", client);
}
else
{
GameMain.Server.SendConsoleMessage(ev.GetDebugInfo(), client);
}
}
}
//GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console);
});
);
commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) =>
{
@@ -2401,7 +2424,7 @@ namespace Barotrauma
}
}));
commands.Add(new Command("sendchatmessage", "Sends a chat message with specified type and color.", (string[] args) =>
commands.Add(new Command("sendchatmessage", "sendchatmessage [sendername] [message] [type] [r] [g] [b] [a]: Sends a chat message with specified type and color.", (string[] args) =>
{
if (args.Length < 2) { return; }
@@ -132,7 +132,7 @@ namespace Barotrauma
else
{
outmsg.WriteUInt16(speaker?.ID ?? Entity.NullEntityID);
outmsg.WriteString(Text ?? string.Empty);
outmsg.WriteString(GetDisplayText()?.Value ?? string.Empty);
outmsg.WriteBoolean(FadeToBlack);
outmsg.WriteByte((byte)Options.Count);
for (int i = 0; i < Options.Count; i++)
@@ -0,0 +1,55 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
partial class EventLogAction : EventAction
{
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
{
if (eventLog == null) { return; }
if (!TargetTag.IsEmpty)
{
List<Client> targetClients = new List<Client>();
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (target is Character character)
{
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (ownerClient != null && eventLog != null)
{
targetClients.Add(ownerClient);
}
}
else
{
DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.");
}
}
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog)
{
Log(targetClients);
}
}
else
{
if (eventLog != null && eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog)
{
Log(targetClients: null);
}
}
void Log(List<Client>? targetClients)
{
string clientStr = targetClients == null || targetClients.None() ?
string.Empty :
$" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})";
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
}
}
}
@@ -0,0 +1,36 @@
namespace Barotrauma
{
partial class EventObjectiveAction : EventAction
{
partial void UpdateProjSpecific()
{
if (GameMain.Server == null) { return; }
EventManager.NetEventObjective objective = new EventManager.NetEventObjective(
Type,
Identifier,
ObjectiveTag,
TextTag,
ParentObjectiveId,
CanBeCompleted);
if (TargetTag.IsEmpty)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
if (client.Character == null) { continue; }
EventManager.ServerWriteObjective(client, objective);
}
}
else
{
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (target is not Character character) { continue; }
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (ownerClient == null) { continue; }
EventManager.ServerWriteObjective(ownerClient, objective);
}
}
}
}
}
@@ -0,0 +1,22 @@
#nullable enable
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma;
partial class EventLog
{
public bool TryAddEntry(Identifier eventPrefabId, Identifier entryId, string text, IEnumerable<Client> targetClients)
{
if (TryAddEntryInternal(eventPrefabId, entryId, text))
{
foreach (var targetClient in targetClients)
{
EventManager.ServerWriteEventLog(targetClient, new EventManager.NetEventLogEntry(eventPrefabId, entryId, text));
}
return true;
}
return false;
}
}
@@ -1,12 +1,29 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class EventManager
{
public static void ServerWriteEventLog(Client client, NetEventLogEntry entry)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)NetworkEventType.EVENTLOG);
outmsg.WriteNetSerializableStruct(entry);
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
public static void ServerWriteObjective(Client client, NetEventObjective entry)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)NetworkEventType.EVENTOBJECTIVE);
outmsg.WriteNetSerializableStruct(entry);
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
public void ServerRead(IReadMessage inc, Client sender)
{
UInt16 actionId = inc.ReadUInt16();
@@ -16,14 +33,14 @@ namespace Barotrauma
{
if (ev is not ScriptedEvent scriptedEvent) { continue; }
var actions = FindActions(scriptedEvent);
foreach (EventAction action in actions.Select(a => a.Item2))
var actions = scriptedEvent.GetAllActions();
foreach (EventAction action in actions.Select(a => a.action))
{
if (action is not 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.");
DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them ({convAction.Text}).");
#endif
continue;
}
@@ -1,4 +1,7 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -20,6 +23,99 @@ namespace Barotrauma
GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage);
}
public static int DistributeRewardsToCrew(IEnumerable<Character> crew, int totalReward)
{
int remainingRewards = totalReward;
float sum = GetRewardDistibutionSum(crew);
if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; }
foreach (Character character in crew)
{
int rewardDistribution = character.Wallet.RewardDistribution;
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int reward = Math.Min(remainingRewards, (int)(totalReward * rewardWeight));
character.Wallet.Give(reward);
remainingRewards -= reward;
if (remainingRewards <= 0) { break; }
}
return remainingRewards;
}
partial void DistributeExperienceToCrew(IEnumerable<Character> crew, int experienceGain)
{
Dictionary<Character, float> traitorExpSteal = new Dictionary<Character, float>();
float totalExpSteal = 0.0f;
foreach (var traitorEvent in GameMain.Server.TraitorManager.ActiveEvents)
{
if (traitorEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) { continue; }
if (traitorEvent.Traitor?.Character == null || !GameMain.Server.ConnectedClients.Contains(traitorEvent.Traitor)) { continue; }
float expSteal = Math.Max(traitorEvent.TraitorEvent.Prefab.StealPercentageOfExperience, 0.0f);
AddTraitorExpSteal(traitorEvent.Traitor.Character, expSteal);
foreach (var secondaryTraitor in traitorEvent.TraitorEvent.SecondaryTraitors)
{
AddTraitorExpSteal(secondaryTraitor.Character, expSteal);
}
void AddTraitorExpSteal(Character traitorCharacter, float expSteal)
{
if (traitorCharacter == null) { return; }
if (!traitorExpSteal.ContainsKey(traitorCharacter))
{
traitorExpSteal.Add(traitorCharacter, 0.0f);
}
traitorExpSteal[traitorCharacter] += expSteal;
}
}
totalExpSteal = traitorExpSteal.Values.Sum();
//if exp to steal exceeds 100%, normalize to get it back to 100%
//(e.g. two traitors who both steal 75%, they'll share 50% of all the exp gains)
if (totalExpSteal > 100.0f)
{
foreach (Character traitor in traitorExpSteal.Keys)
{
traitorExpSteal[traitor] /= totalExpSteal;
}
totalExpSteal = 100.0f;
}
if (totalExpSteal > 0)
{
GameServer.Log($"Traitors stole {(int)totalExpSteal}% of the total experience.", ServerLog.MessageType.Traitors);
}
int nonTraitorCount = GameSession.GetSessionCrewCharacters(CharacterType.Both).Count(c => !traitorExpSteal.ContainsKey(c));
foreach (Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo);
}
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
{
GiveMissionExperience(bot.Info);
}
void GiveMissionExperience(CharacterInfo info)
{
if (info == null) { return; }
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value);
if (info.Character != null && traitorExpSteal.TryGetValue(info.Character, out float expToSteal))
{
int stealAmount = (int)(experienceGain * nonTraitorCount * expToSteal / 100.0f);
GameServer.Log($"Traitor {info.Character} stole {stealAmount} ({(int)expToSteal}%) of the total experience.", ServerLog.MessageType.Traitors);
finalExperienceGain += stealAmount;
}
else
{
GameServer.Log($"{(int)(finalExperienceGain * totalExpSteal / 100.0f)} ({(int)totalExpSteal}%) was stolen from {info.Name}.", ServerLog.MessageType.Traitors);
finalExperienceGain -= (int)(finalExperienceGain * totalExpSteal / 100.0f);
}
info.GiveExperience(finalExperienceGain);
}
}
public virtual void ServerWriteInitial(IWriteMessage msg, Client c)
{
msg.WriteUInt16((ushort)State);
@@ -4,8 +4,6 @@ namespace Barotrauma
{
partial class NestMission : Mission
{
private Level.Cave selectedCave;
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
base.ServerWriteInitial(msg, c);
@@ -327,6 +327,7 @@ namespace Barotrauma
while (Timing.Accumulator >= Timing.Step)
{
Timing.TotalTime += Timing.Step;
Timing.TotalTimeUnpaused += Timing.Step;
DebugConsole.Update();
if (GameSession?.GameMode == null || !GameSession.GameMode.Paused)
{
@@ -42,10 +42,13 @@ namespace Barotrauma
}
}
public void Refresh(Character character)
public void Refresh(Character character, bool refreshHealthData)
{
healthData = new XElement("health");
character.CharacterHealth.Save(healthData);
if (refreshHealthData)
{
healthData = new XElement("health");
character.CharacterHealth.Save(healthData);
}
if (character.Inventory != null)
{
itemData = new XElement("inventory");
@@ -260,7 +260,7 @@ namespace Barotrauma
{
//character still alive (or killed by Disconnect) -> save it as-is
characterData.RemoveAll(cd => cd.IsDuplicate(data));
data.Refresh(character);
data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected);
characterData.Add(data);
}
else
@@ -318,7 +318,7 @@ namespace Barotrauma
discardedCharacters.Clear();
}
protected override IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults)
protected override IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
{
IncrementAllLastUpdateIds();
@@ -360,7 +360,7 @@ namespace Barotrauma
GameMain.GameSession.EventManager.RegisterEventHistory();
}
GameMain.GameSession.EndRound("", traitorResults, transitionType);
GameMain.GameSession.EndRound("", transitionType);
//--------------------------------------
@@ -1361,6 +1361,10 @@ namespace Barotrauma
modeElement.Add(Settings.Save());
modeElement.Add(SaveStats());
if (GameMain.Server?.TraitorManager is TraitorManager traitorManager)
{
modeElement.Add(traitorManager.Save());
}
modeElement.Add(Bank.Save());
if (GameMain.GameSession?.EventManager != null)
@@ -0,0 +1,12 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class CharacterInventory : Inventory
{
public void ServerEventWrite(IWriteMessage msg, Client c, Character.InventoryStateEventData inventoryData)
{
SharedWrite(msg, inventoryData.SlotRange);
}
}
}
@@ -32,6 +32,7 @@ namespace Barotrauma.Items.Components
Drop(false, null);
item.SetTransform(simPosition, 0.0f, findNewHull: false);
AttachToWall();
OnUsed.Invoke(new ItemUseInfo(item, c.Character));
item.CreateServerEvent(this);
c.Character.Inventory?.CreateNetworkEvent();
@@ -49,21 +49,35 @@ namespace Barotrauma.Items.Components
msg.WriteSingle(jointAxis.Y);
if (StickTarget.UserData is Structure structure)
{
msg.WriteByte((byte)StickTargetType.Structure);
msg.WriteUInt16(structure.ID);
int bodyIndex = structure.Bodies.IndexOf(StickTarget);
msg.WriteByte((byte)(bodyIndex == -1 ? 0 : bodyIndex));
}
else if (StickTarget.UserData is Entity entity)
else if (StickTarget.UserData is Item item)
{
msg.WriteUInt16(entity.ID);
msg.WriteByte((byte)StickTargetType.Item);
msg.WriteUInt16(item.ID);
}
else if (StickTarget.UserData is Submarine sub)
{
msg.WriteByte((byte)StickTargetType.Submarine);
msg.WriteUInt16(sub.ID);
}
else if (StickTarget.UserData is Limb limb)
{
msg.WriteByte((byte)StickTargetType.Limb);
msg.WriteUInt16(limb.character.ID);
msg.WriteByte((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb));
}
else if (StickTarget.UserData is Voronoi2.VoronoiCell cell)
{
msg.WriteByte((byte)StickTargetType.LevelWall);
msg.WriteInt32(Level.Loaded.GetAllCells().IndexOf(cell));
}
else
{
msg.WriteByte((byte)StickTargetType.Unknown);
throw new NotImplementedException(StickTarget.UserData?.ToString() ?? "null" + " is not a valid projectile stick target.");
}
}
@@ -45,8 +45,7 @@ namespace Barotrauma.Items.Components
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.WriteSingle(deteriorationTimer);
msg.WriteSingle(deteriorateAlwaysResetTimer);
msg.WriteBoolean(DeteriorateAlways);
msg.WriteSingle(ForceDeteriorationTimer);
msg.WriteSingle(tinkeringDuration);
msg.WriteSingle(tinkeringStrength);
msg.WriteBoolean(tinkeringPowersDevices);
@@ -0,0 +1,320 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
internal sealed partial class CircuitBox
{
/// <summary>
/// If the server needs to initialize the circuit box to the clients
/// instead of the clients loading it from the save file.
/// </summary>
private bool needsServerInitialization;
/// <summary>
/// When in multiplayer and the circuit box is loaded from the players inventory,
/// We only load the components from XML on server side since only the server has access to CharacterCampaignData
/// and then send a network event syncing the loaded properties. But circuit box properties are too complex to
/// sync using the existing syncing logic so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
/// </summary>
public void MarkServerRequiredInitialization()
=> needsServerInitialization = true;
public partial void OnDeselected(Character c)
{
ClearAllSelectionsInternal(c.ID);
BroadcastSelectionStatus();
}
public void ServerRead(INetSerializableStruct data, Client c)
{
switch (data)
{
case NetCircuitBoxCursorInfo { RecordedPositions.Length: 10 } cursorInfo:
{
RelayCursorState(cursorInfo, c);
break;
}
}
}
private void RelayCursorState(NetCircuitBoxCursorInfo data, Client sender)
{
if (GameMain.Server is null || !IsRoundRunning()) { return; }
SendToAll(CircuitBoxOpcode.Cursor, data with { CharacterID = sender.CharacterID }, FilterClients);
bool FilterClients(Client client)
{
// ReSharper disable once RedundantAssignment
bool isSender = client == sender;
#if DEBUG
// Shown own cursor in debug builds
isSender = false;
#endif
return !isSender && client.Character is not null && client.Character.SelectedItem == item;
}
}
public void SendToClient(CircuitBoxOpcode opcode, INetSerializableStruct data, Client targetClient)
{
var (msg, deliveryMethod) = PrepareToSend(opcode, data);
GameMain.Server?.ServerPeer?.Send(msg, targetClient.Connection, deliveryMethod);
}
public void SendToAll(CircuitBoxOpcode opcode, INetSerializableStruct data, Func<Client, bool>? predicate = null)
{
var (msg, deliveryMethod) = PrepareToSend(opcode, data);
foreach (Client client in GameMain.Server.ConnectedClients)
{
if (predicate is not null && !predicate(client)) { continue; }
GameMain.Server?.ServerPeer?.Send(msg, client.Connection, deliveryMethod);
}
}
private (IWriteMessage Message, DeliveryMethod DeliveryMethod) PrepareToSend(CircuitBoxOpcode opcode, INetSerializableStruct data)
{
IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CIRCUITBOX);
msg.WriteNetSerializableStruct(new NetCircuitBoxHeader(
Opcode: opcode,
ItemID: item.ID,
ComponentIndex: (byte)item.GetComponentIndex(this)));
msg.WriteNetSerializableStruct(data);
DeliveryMethod deliveryMethod =
UnrealiableOpcodes.Contains(opcode)
? DeliveryMethod.Unreliable
: DeliveryMethod.Reliable;
return (msg, deliveryMethod);
}
public void CreateServerEvent(INetSerializableStruct data)
=> item.CreateServerEvent(this, new CircuitBoxEventData(data));
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null)
{
if (extraData is null) { return; }
var eventData = ExtractEventData<CircuitBoxEventData>(extraData);
msg.WriteByte((byte)eventData.Opcode);
msg.WriteNetSerializableStruct(eventData.Data);
}
public void ServerEventRead(IReadMessage msg, Client c)
{
var header = (CircuitBoxOpcode)msg.ReadByte();
switch (header)
{
case CircuitBoxOpcode.AddComponent:
{
var data = INetSerializableStruct.Read<CircuitBoxAddComponentEvent>(msg);
if (!item.CanClientAccess(c)) { break; }
var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier);
if (prefab is null)
{
ThrowError("Unable to add component because the prefab was not found.", c);
return;
}
if (IsFull || !GetApplicableResourcePlayerHas(prefab, c.Character).TryUnwrap(out var resource)) { return; }
ushort id = ICircuitBoxIdentifiable.FindFreeID(Components);
if (id is ICircuitBoxIdentifiable.NullComponentID)
{
ThrowError("Unable to add component because there are no available IDs left.", c);
return;
}
bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, it =>
{
CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position));
});
if (!result)
{
ThrowError("Unable to add component because the component could not be created.", c);
return;
}
GameServer.Log($"{NetworkMember.ClientLogName(c)} added a {prefab.Name} into a circuit box.", ServerLog.MessageType.Wiring);
RemoveItem(resource);
break;
}
case CircuitBoxOpcode.MoveComponent:
{
var data = INetSerializableStruct.Read<CircuitBoxMoveComponentEvent>(msg);
if (!item.CanClientAccess(c)) { break; }
MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount);
CreateServerEvent(data);
break;
}
case CircuitBoxOpcode.DeleteComponent:
{
var data = INetSerializableStruct.Read<CircuitBoxRemoveComponentEvent>(msg);
if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; }
CreateRefundItemsForUsedResources(data.TargetIDs, c.Character);
GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring);
RemoveComponentInternal(data.TargetIDs);
CreateServerEvent(data);
break;
}
case CircuitBoxOpcode.SelectComponents:
{
var data = INetSerializableStruct.Read<CircuitBoxSelectNodesEvent>(msg);
if (!item.CanClientAccess(c)) { break; }
SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite);
SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite);
BroadcastSelectionStatus();
break;
}
case CircuitBoxOpcode.SelectWires:
{
var data = INetSerializableStruct.Read<CircuitBoxSelectWiresEvent>(msg);
if (!item.CanClientAccess(c)) { break; }
SelectWiresInternal(data.TargetIDs, c.CharacterID, data.Overwrite);
BroadcastSelectionStatus();
break;
}
case CircuitBoxOpcode.AddWire:
{
var data = INetSerializableStruct.Read<CircuitBoxClientAddWireEvent>(msg);
if (!item.CanClientAccess(c)) { break; }
var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier);
if (prefab is null)
{
ThrowError($"Unable to connect wire because wire by identifier \"{data.SelectedWirePrefabIdentifier}\" was not found.", c);
break;
}
if (data.Start.FindConnection(this).TryUnwrap(out var start) &&
data.End.FindConnection(this).TryUnwrap(out var end))
{
bool result = Connect(start, end, wire =>
{
CreateServerEvent(new CircuitBoxServerCreateWireEvent(data with { Start = wire.Start, End = wire.End }, wire.ID, wire.Item.Select(static i => i.ID)));
}, prefab);
if (!result)
{
ThrowError("Unable to connect wire because the circuit box rejected it.", c);
}
GameServer.Log($"{NetworkMember.ClientLogName(c)} connected a wire from {start.Name} to {end.Name} in a circuit box.", ServerLog.MessageType.Wiring);
}
else
{
ThrowError($"Unable to connect wire because the start or end connection was not found. (start: {data.Start}, end: {data.End})", c);
}
break;
}
case CircuitBoxOpcode.RemoveWire:
{
var data = INetSerializableStruct.Read<CircuitBoxRemoveWireEvent>(msg);
if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; }
GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring);
RemoveWireInternal(data.TargetIDs);
CreateServerEvent(data);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
}
string GetLogComponentName(IReadOnlyList<ushort> ids)
{
if (ids.Count > 1) { return $"{ids.Count} components"; }
return Components.FirstOrDefault(comp => ids.Contains(comp.ID))?.Item.Name ?? "[UNKNOWN]";
}
string GetLogWireName(IReadOnlyList<ushort> ids)
{
if (ids.Count > 1) { return $"{ids.Count} wires"; }
if (Wires.FirstOrDefault(w => ids.Contains(w.ID)) is not { } wire) { return "[UNKNOWN]"; }
return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire";
}
}
/// <summary>
/// Creates an event that overrides the state of the circuit box for all clients.
/// This is only required if the circuit box is loaded from the players inventory in multiplayer.
/// </summary>
public void CreateInitializationEvent()
{
Vector2 inputPos = Vector2.Zero,
outputPos = Vector2.Zero;
foreach (var ioNode in InputOutputNodes)
{
switch (ioNode.NodeType)
{
case CircuitBoxInputOutputNode.Type.Input:
inputPos = ioNode.Position;
break;
case CircuitBoxInputOutputNode.Type.Output:
outputPos = ioNode.Position;
break;
}
}
CircuitBoxInitializeStateFromServerEvent data = new(
Components: Components.Select(EventFromComponent).ToImmutableArray(),
Wires: Wires.Select(EventFromWire).ToImmutableArray(),
InputPos: inputPos,
OutputPos: outputPos);
CreateServerEvent(data);
static CircuitBoxServerCreateComponentEvent EventFromComponent(CircuitBoxComponent component)
=> new(component.Item.ID, component.UsedResource.UintIdentifier, component.ID, component.Position);
static CircuitBoxServerCreateWireEvent EventFromWire(CircuitBoxWire wire)
{
var backingWire = wire.BackingWire.Select(static i => i.ID);
var from = CircuitBoxConnectorIdentifier.FromConnection(wire.From);
var to = CircuitBoxConnectorIdentifier.FromConnection(wire.To);
var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier);
return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire);
}
}
// we don't care about updating the view on server
public partial void OnViewUpdateProjSpecific() { }
private void ThrowError(string message, Client c)
{
DebugConsole.ThrowError(message);
SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c);
}
private void BroadcastSelectionStatus()
{
var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray();
var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray();
var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray();
CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios));
}
}
}
@@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components
List<Wire>[] wires = new List<Wire>[Connections.Count];
//read wire IDs for each connection
for (int i = 0; i < Connections.Count; i++)
byte connectionCount = msg.ReadByte();
for (int i = 0; i < Connections.Count && i < connectionCount; i++)
{
wires[i] = new List<Wire>();
uint wireCount = msg.ReadVariableUInt32();
@@ -32,17 +32,17 @@ namespace Barotrauma.Items.Components
GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name,
ServerLog.MessageType.ItemInteraction);
OutputValue = newOutputValue;
ShowOnDisplay(newOutputValue, addToHistory: true, TextColor);
ShowOnDisplay(newOutputValue, addToHistory: true, TextColor, isWelcomeMessage: false);
item.SendSignal(newOutputValue, "signal_out");
item.CreateServerEvent(this);
}
}
partial void ShowOnDisplay(string input, bool addToHistory, Color color)
partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage)
{
if (addToHistory)
{
messageHistory.Add(new TerminalMessage(input, color));
messageHistory.Add(new TerminalMessage(input, color, isWelcomeMessage));
while (messageHistory.Count > MaxMessages)
{
messageHistory.RemoveAt(0);
@@ -54,9 +54,11 @@ namespace Barotrauma.Items.Components
{
//split too long messages to multiple parts
int msgIndex = 0;
foreach (var (str, _) in messageHistory)
foreach (var msg in messageHistory)
{
string msgToSend = str;
//the clients create the welcome message themselves, no need to sync it
if (msg.IsWelcomeMessage) { continue; }
string msgToSend = msg.Text;
if (string.IsNullOrEmpty(msgToSend))
{
item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend));
@@ -4,17 +4,25 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
partial class Inventory : IServerSerializable, IClientSerializable
partial class Inventory : IClientSerializable
{
private readonly Dictionary<Client, List<ushort>[]> receivedItemIds = new Dictionary<Client, List<ushort>[]>();
public void ServerEventRead(IReadMessage msg, Client c)
{
List<Item> prevItems = new List<Item>(AllItems.Distinct());
SharedRead(msg, out var newItemIDs);
if (!receivedItemIds.TryGetValue(c, out List<ushort>[] receivedItemIdsFromClient))
{
receivedItemIdsFromClient = new List<ushort>[capacity];
receivedItemIds.Add(c, receivedItemIdsFromClient);
}
SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply);
if (!readyToApply) { return; }
if (c == null || c.Character == null) { return; }
@@ -39,7 +47,7 @@ namespace Barotrauma
CreateNetworkEvent();
for (int i = 0; i < capacity; i++)
{
foreach (ushort id in newItemIDs[i])
foreach (ushort id in receivedItemIdsFromClient[i])
{
if (Entity.FindEntityByID(id) is not Item item) { continue; }
item.PositionUpdateInterval = 0.0f;
@@ -58,7 +66,7 @@ namespace Barotrauma
{
foreach (Item item in slots[i].Items.ToList())
{
if (!newItemIDs[i].Contains(item.ID))
if (!receivedItemIdsFromClient[i].Contains(item.ID))
{
Item droppedItem = item;
Entity prevOwner = Owner;
@@ -85,7 +93,7 @@ namespace Barotrauma
}
}
foreach (ushort id in newItemIDs[i])
foreach (ushort id in receivedItemIdsFromClient[i])
{
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
prevItemInventories.Add(newItem?.ParentInventory);
@@ -94,7 +102,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
foreach (ushort id in newItemIDs[i])
foreach (ushort id in receivedItemIdsFromClient[i])
{
if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; }
@@ -128,7 +136,7 @@ namespace Barotrauma
TryPutItem(item, i, true, true, c.Character, false);
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item) && !newItemIDs[j].Contains(item.ID))
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
{
slots[j].RemoveItem(item);
}
@@ -138,42 +146,48 @@ namespace Barotrauma
EnsureItemsInBothHands(c.Character);
receivedItemIds.Remove(c);
CreateNetworkEvent();
foreach (Inventory prevInventory in prevItemInventories.Distinct())
{
if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); }
}
foreach (Item item in AllItems.Distinct())
foreach (Item item in AllItems.DistinctBy(it => it.Prefab))
{
if (item == null) { continue; }
if (!prevItems.Contains(item))
{
int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it));
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == c.Character)
{
HumanAIController.ItemTaken(item, c.Character);
GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory);
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log(GameServer.CharacterLogName(c.Character) + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory);
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory);
}
}
}
foreach (Item item in prevItems.Distinct())
var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it));
foreach (Item item in droppedItems.DistinctBy(it => it.Prefab))
{
if (item == null) { continue; }
if (!AllItems.Contains(item))
var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it));
int amount = matchingItems.Count();
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
if (Owner == c.Character)
{
if (Owner == c.Character)
{
GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + item.Name, ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log(GameServer.CharacterLogName(c.Character) + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory);
}
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory);
}
item.CreateDroppedStack(matchingItems, allowClientExecute: true);
}
}
@@ -203,10 +217,5 @@ namespace Barotrauma
bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length;
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
SharedWrite(msg, extraData);
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -18,9 +19,23 @@ namespace Barotrauma
get { return base.Prefab?.Sprite; }
}
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType)
private readonly Dictionary<Client, CampaignMode.InteractionType> campaignInteractionTypePerClient = new Dictionary<Client, CampaignMode.InteractionType>();
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients)
{
GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData());
if (Removed) { return; }
if (targetClients == null || targetClients.None())
{
campaignInteractionTypePerClient.Clear();
}
else
{
foreach (Client client in targetClients)
{
campaignInteractionTypePerClient[client] = interactionType;
}
}
GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData(targetClients));
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -33,7 +48,7 @@ namespace Barotrauma
}
if (extraData is null) { throw error("event data was null"); }
if (!(extraData is IEventData itemEventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); }
if (extraData is not IEventData itemEventData) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); }
msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue);
switch (itemEventData)
@@ -44,7 +59,7 @@ namespace Barotrauma
{
throw error($"component index out of range ({componentIndex})");
}
if (!(components[componentIndex] is IServerSerializable serializableComponent))
if (components[componentIndex] is not IServerSerializable serializableComponent)
{
throw error($"component \"{components[componentIndex]}\" is not server serializable");
}
@@ -57,20 +72,28 @@ namespace Barotrauma
{
throw error($"container index out of range ({containerIndex})");
}
if (!(components[containerIndex] is ItemContainer itemContainer))
if (components[containerIndex] is not ItemContainer itemContainer)
{
throw error("component \"" + components[containerIndex] + "\" is not server serializable");
}
msg.WriteRangedInteger(containerIndex, 0, components.Count - 1);
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
itemContainer.Inventory.ServerEventWrite(msg, c);
itemContainer.Inventory.ServerEventWrite(msg, c, inventoryStateEventData);
break;
case ItemStatusEventData statusEvent:
msg.WriteBoolean(statusEvent.LoadingRound);
msg.WriteSingle(condition);
break;
case AssignCampaignInteractionEventData _:
msg.WriteByte((byte)CampaignInteractionType);
case AssignCampaignInteractionEventData campaignInteractionData:
bool isVisibleToClient =
campaignInteractionData.TargetClients == null ||
campaignInteractionData.TargetClients.IsEmpty ||
campaignInteractionData.TargetClients.Contains(c);
msg.WriteBoolean(isVisibleToClient);
if (isVisibleToClient)
{
msg.WriteByte((byte)CampaignInteractionType);
}
break;
case ApplyStatusEffectEventData applyStatusEffectEventData:
{
@@ -131,6 +154,13 @@ namespace Barotrauma
}
}
break;
case DroppedStackEventData droppedStackEventData:
msg.WriteRangedInteger(droppedStackEventData.Items.Length, 0, Inventory.MaxPossibleStackSize);
foreach (Item droppedItem in droppedStackEventData.Items)
{
msg.WriteUInt16(droppedItem.ID);
}
break;
default:
throw error($"Unsupported event type {itemEventData.GetType().Name}");
}
@@ -257,10 +287,10 @@ namespace Barotrauma
msg.WriteInt32(idCardComponent.SubmarineSpecificID);
msg.WriteString(idCardComponent.OwnerName);
msg.WriteString(idCardComponent.OwnerTags);
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex + 1));
msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor);
msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor);
msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor);
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma;
partial class Item
{
private readonly struct DroppedStackEventData : IEventData
{
public EventType EventType => EventType.DroppedStack;
public readonly ImmutableArray<Item> Items;
public DroppedStackEventData(IEnumerable<Item> items)
{
Items = items.Distinct().ToImmutableArray();
}
}
}
@@ -0,0 +1,13 @@
using Barotrauma.Networking;
using System;
namespace Barotrauma
{
partial class ItemInventory : Inventory
{
public void ServerEventWrite(IWriteMessage msg, Client c, Item.InventoryStateEventData inventoryData)
{
SharedWrite(msg, inventoryData.SlotRange);
}
}
}
@@ -176,6 +176,7 @@ namespace Barotrauma
BackgroundSections[i].SetColor(color);
},
out int sectorToUpdate);
RefreshAveragePaintedColor();
//add to pending updates to notify other clients as well
pendingSectionUpdates.Add(sectorToUpdate);
break;
@@ -42,8 +42,6 @@ namespace Barotrauma.Networking
public string RejectedName;
public int RoundsSincePlayedAsTraitor;
public float KickAFKTimer;
public double MidRoundSyncTimeOut;
@@ -49,7 +49,7 @@ namespace Barotrauma.Networking
{
await Task.Yield();
string dir = mod.Dir;
SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod), fileName => { });
SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod));
}
private void DeleteDir()
@@ -74,13 +74,21 @@ namespace Barotrauma.Networking
private bool initiatedStartGame;
private CoroutineHandle startGameCoroutine;
public TraitorManager TraitorManager;
private readonly ServerEntityEventManager entityEventManager;
public FileSender FileSender { get; private set; }
public ModSender ModSender { get; private set; }
private TraitorManager traitorManager;
public TraitorManager TraitorManager
{
get
{
traitorManager ??= new TraitorManager(this);
return traitorManager;
}
}
#if DEBUG
public void PrintSenderTransters()
@@ -358,22 +366,31 @@ namespace Barotrauma.Networking
for (int i = Character.CharacterList.Count - 1; i >= 0; i--)
{
Character character = Character.CharacterList[i];
if (character.IsDead || !character.ClientDisconnected) { continue; }
character.KillDisconnectedTimer += deltaTime;
character.SetStun(1.0f);
if (!character.ClientDisconnected) { continue; }
Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime)
if (!character.IsDead)
{
character.Kill(CauseOfDeathType.Disconnected, null);
continue;
}
character.KillDisconnectedTimer += deltaTime;
character.SetStun(1.0f);
if (owner != null && owner.InGame && !owner.NeedsMidRoundSync &&
(!ServerSettings.AllowSpectating || !owner.SpectateOnly))
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime)
{
character.Kill(CauseOfDeathType.Disconnected, null);
continue;
}
if (owner != null && owner.InGame && !owner.NeedsMidRoundSync &&
(!ServerSettings.AllowSpectating || !owner.SpectateOnly))
{
SetClientCharacter(owner, character);
}
}
else if (owner != null &&
character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected &&
character.CharacterHealth.VitalityDisregardingDeath > 0)
{
SetClientCharacter(owner, character);
character.Revive(removeAfflictions: false);
}
}
@@ -412,12 +429,7 @@ namespace Barotrauma.Networking
}
float endRoundDelay = 1.0f;
if (TraitorManager?.ShouldEndRound ?? false)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
}
else if (ServerSettings.AutoRestart && isCrewDead)
if (ServerSettings.AutoRestart && isCrewDead)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
@@ -452,11 +464,7 @@ namespace Barotrauma.Networking
if (endRoundTimer >= endRoundDelay)
{
if (TraitorManager?.ShouldEndRound ?? false)
{
Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage);
}
else if (ServerSettings.AutoRestart && isCrewDead)
if (ServerSettings.AutoRestart && isCrewDead)
{
Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage);
}
@@ -830,6 +838,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.MEDICAL:
ReadMedicalMessage(inc, connectedClient);
break;
case ClientPacketHeader.CIRCUITBOX:
ReadCircuitBoxMessage(inc, connectedClient);
break;
case ClientPacketHeader.READY_CHECK:
ReadyCheck.ServerRead(inc, connectedClient);
break;
@@ -1299,6 +1310,22 @@ namespace Barotrauma.Networking
}
}
private static void ReadCircuitBoxMessage(IReadMessage inc, Client sender)
{
var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
INetSerializableStruct data = header.Opcode switch
{
CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
_ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.")
};
if (header.FindTarget().TryUnwrap(out var box))
{
box.ServerRead(data, sender);
}
}
private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender)
{
sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
@@ -1775,7 +1802,7 @@ namespace Barotrauma.Networking
while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0)
{
var entity = c.PendingPositionUpdates.Peek();
if (!(entity is IServerPositionSync entityPositionSync) ||
if (entity is not IServerPositionSync entityPositionSync ||
entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
{
@@ -1957,7 +1984,8 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2);
outmsg.WriteSingle(ServerSettings.TraitorProbability);
outmsg.WriteRangedInteger(ServerSettings.TraitorDangerLevel, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel);
outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All);
@@ -2212,6 +2240,7 @@ namespace Barotrauma.Networking
//don't instantiate a new gamesession if we're playing a campaign
if (campaign == null || GameMain.GameSession == null)
{
traitorManager = new TraitorManager(this);
GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType);
}
else
@@ -2509,16 +2538,8 @@ namespace Barotrauma.Networking
}
}
TraitorManager = null;
if (ServerSettings.TraitorsEnabled == YesNoMaybe.Yes ||
(ServerSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f))
{
if (!(GameMain.GameSession?.GameMode is CampaignMode))
{
TraitorManager = new TraitorManager();
TraitorManager.Start(this);
}
}
TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded);
TraitorManager.Enabled = Rand.Range(0.0f, 1.0f) < ServerSettings.TraitorProbability;
GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled"));
@@ -2562,7 +2583,6 @@ namespace Barotrauma.Networking
msg.WriteBoolean(ServerSettings.AllowRewiring);
msg.WriteBoolean(ServerSettings.AllowFriendlyFire);
msg.WriteBoolean(ServerSettings.LockAllDefaultWires);
msg.WriteBoolean(ServerSettings.AllowRagdollButton);
msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat);
msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest);
msg.WriteBoolean(IsUsingRespawnShuttle());
@@ -2685,13 +2705,12 @@ namespace Barotrauma.Networking
}
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
var traitorResults = TraitorManager?.GetEndResults() ?? new List<TraitorMissionResult>();
List<Mission> missions = GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession is { IsRunning: true })
{
GameMain.GameSession.EndRound(endMessage, traitorResults);
GameMain.GameSession.EndRound(endMessage);
}
TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null;
endRoundTimer = 0.0f;
@@ -2736,10 +2755,10 @@ namespace Barotrauma.Networking
}
msg.WriteByte(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam);
msg.WriteByte((byte)traitorResults.Count);
foreach (var traitorResult in traitorResults)
msg.WriteBoolean(traitorResults.HasValue);
if (traitorResults.HasValue)
{
traitorResult.ServerWrite(msg);
msg.WriteNetSerializableStruct(traitorResults.Value);
}
foreach (Client client in connectedClients)
@@ -2788,13 +2807,14 @@ namespace Barotrauma.Networking
if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; }
var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime;
if (timeSinceNameChange < Client.NameChangeCoolDown)
if (timeSinceNameChange < Client.NameChangeCoolDown && newName != c.Name)
{
//only send once per second at most to prevent using this for spamming
if (timeSinceNameChange.TotalSeconds > 1)
{
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
LastClientListUpdateID++;
}
c.NameId = nameId;
c.RejectedName = newName;
@@ -3558,14 +3578,9 @@ namespace Barotrauma.Networking
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
}
public void SendTraitorMessage(Client client, string message, Identifier missionIdentifier, TraitorMessageType messageType)
public void SendTraitorMessage(WriteOnlyMessage msg, Client client)
{
if (client == null) { return; }
var msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE);
msg.WriteByte((byte)messageType);
msg.WriteIdentifier(missionIdentifier);
msg.WriteString(message);
if (client == null) { return; };
serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered);
}
@@ -240,19 +240,20 @@ namespace Barotrauma
{
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner);
Character yoinkerCharacter = yoinker?.Character;
Character thiefCharacter = yoinker?.Character;
Character targetCharacter = inventory.Owner as Character;
if (yoinker == null || item == null || yoinkerCharacter == null || targetCharacter == null || yoinkerCharacter == targetCharacter) { return; }
if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; }
if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; }
// Only if the target is alive and they are stunned, unconscious or handcuffed
if (targetCharacter.IsDead || targetCharacter.Removed || !(targetCharacter.Stun > 0) && !targetCharacter.IsUnconscious && !targetCharacter.LockHands) { return; }
if (GameMain.Server.TraitorManager?.Traitors != null)
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == targetCharacter || t.Character == yoinkerCharacter))
if (GameMain.Server.TraitorManager.IsTraitor(targetCharacter) ||
GameMain.Server.TraitorManager.IsTraitor(thiefCharacter))
{
// Don't penalize traitors
return;
@@ -299,7 +300,7 @@ namespace Barotrauma
}
// Name tag doesn't belong to anyone in particular or we own the ID card
if (name == null || name == yoinkerCharacter.Name) { return; }
if (name == null || name == thiefCharacter.Name) { return; }
}
if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; }
@@ -329,7 +330,7 @@ namespace Barotrauma
karmaDecrease *= 0.5f;
}
AdjustKarma(yoinkerCharacter, -karmaDecrease, "Stolen dangerous item");
AdjustKarma(thiefCharacter, -karmaDecrease, "Stolen dangerous item");
}
public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable<Affliction> appliedAfflictions = null)
@@ -341,27 +342,23 @@ namespace Barotrauma
if (target.IsDead || target.Removed) { return; }
bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID;
if (GameMain.Server.TraitorManager?.Traitors != null)
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == target))
if (GameMain.Server.TraitorManager.IsTraitor(target))
{
//traitors always count as enemies
isEnemy = true;
}
if (GameMain.Server.TraitorManager.Traitors.Any(t =>
t.Character == attacker &&
t.CurrentObjective != null &&
t.CurrentObjective.IsEnemy(target)))
if (GameMain.Server.TraitorManager.IsTraitor(attacker))
{
//target counts as an enemy to the traitor
//others count as an enemies to the traitor
isEnemy = true;
}
}
bool targetIsHusk = target.CharacterHealth?.GetAffliction<AfflictionHusk>(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active;
bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction<AfflictionHusk>(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active;
static bool IsHusk(Character c) => c.IsHusk || c.IsHuskInfected;
//huskified characters count as enemies to healthy characters and vice versa
if (targetIsHusk != attackerIsHusk) { isEnemy = true; }
if (IsHusk(attacker) != IsHusk(target)) { isEnemy = true; }
if (appliedAfflictions != null)
{
@@ -478,14 +475,11 @@ namespace Barotrauma
if (damageAmount > 0)
{
if (StructureDamageKarmaDecrease <= 0.0f) { return; }
if (GameMain.Server.TraitorManager?.Traitors != null)
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t =>
t.Character == attacker &&
t.CurrentObjective != null &&
t.CurrentObjective.IsAllowedToDamage(structure)))
if (GameMain.Server.TraitorManager.IsTraitor(attacker))
{
//traitor tasked to flood the sub -> damaging structures is ok
//traitors are allowed to damage structures
return;
}
}
@@ -580,7 +574,7 @@ namespace Barotrauma
public void OnItemContained(Item containedItem, Item container, Character character)
{
if (containedItem == null || container == null || character == null || character.IsTraitor) { return; }
if (container.Prefab.Identifier == "weldingtool" && containedItem.HasTag("oxygensource"))
if (container.Prefab.Identifier == Tags.WeldingFuel && containedItem.HasTag(Tags.OxygenSource))
{
var client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (client == null) { return; }
@@ -614,7 +608,7 @@ namespace Barotrauma
if (amount < 0.0f)
{
float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType);
float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType);
var clientMemory = GetClientMemory(client);
clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime);
float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b);
@@ -27,7 +27,8 @@ namespace Barotrauma.Networking
AutoExpandMTU = false,
MaximumConnections = NetConfig.MaxPlayers * 2,
EnableUPnP = serverSettings.EnableUPnP,
Port = serverSettings.Port
Port = serverSettings.Port,
DualStack = GameSettings.CurrentConfig.UseDualModeSockets
};
netPeerConfiguration.DisableMessageType(
@@ -433,9 +433,9 @@ namespace Barotrauma.Networking
}
//tell the respawning client they're no longer a traitor
if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null)
if (GameMain.Server.TraitorManager != null && clients[i].Character != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == clients[i].Character))
if (GameMain.Server.TraitorManager.IsTraitor(clients[i].Character))
{
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("TraitorRespawnMessage"), clients[i], ChatMessageType.ServerMessageBox);
}
@@ -537,18 +537,7 @@ namespace Barotrauma.Networking
}
//add the ID card tags they should've gotten when spawning in the shuttle
foreach (Item item in character.Inventory.AllItems.Distinct())
{
if (item.GetComponent<IdCard>() == null) { continue; }
foreach (string s in shuttleSpawnPoints[i].IdCardTags)
{
item.AddTag(s);
}
if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc))
{
item.Description = shuttleSpawnPoints[i].IdCardDesc;
}
}
character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true);
}
}
@@ -559,7 +548,7 @@ namespace Barotrauma.Networking
{
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier);
if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; }
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath);
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillLossPercentageOnDeath / 100.0f);
}
}
@@ -215,11 +215,15 @@ namespace Barotrauma.Networking
int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits);
int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1;
if (traitorSetting < 0) { traitorSetting = 2; }
if (traitorSetting > 2) { traitorSetting = 0; }
TraitorsEnabled = (YesNoMaybe)traitorSetting;
bool changedTraitorProbability = incMsg.ReadBoolean();
float traitorProbability = incMsg.ReadSingle();
if (changedTraitorProbability)
{
TraitorProbability = traitorProbability;
}
//the byte indicates the direction we're changing the value, subtract one to get negative values from a byte
TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1;
int botCount = BotCount + incMsg.ReadByte() - 1;
if (botCount < 0) { botCount = MaxBotCount; }
@@ -347,7 +351,7 @@ namespace Barotrauma.Networking
selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f);
GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty);
GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled);
GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability);
HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty<string>()));
if (HiddenSubs.Any())
@@ -337,6 +337,20 @@ namespace Barotrauma
sender.SetVote(voteType, (int)inc.ReadByte());
}
break;
case VoteType.Traitor:
int clientId = inc.ReadInt32();
if (sender.InGame && sender.Character != null)
{
var client = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.SessionId == clientId);
sender.SetVote(voteType, client);
if (client?.Character != null)
{
GameMain.Server.SendChatMessage(
TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value,
ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character);
}
}
break;
}
inc.ReadPadBits();
@@ -1,4 +1,5 @@
using System.Linq;
using System.Globalization;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma.Steam
@@ -71,7 +72,7 @@ namespace Barotrauma.Steam
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());
Steamworks.SteamServer.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString());
Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString());
Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString());
Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture));
Steamworks.SteamServer.SetKey("friendlyfireenabled", server.ServerSettings.AllowFriendlyFire.ToString());
Steamworks.SteamServer.SetKey("karmaenabled", server.ServerSettings.KarmaEnabled.ToString());
Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString());
@@ -1,64 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SqlServer.Server;
namespace Barotrauma
{
partial class Traitor
{
public abstract class Goal
{
public HashSet<Traitor> Traitors { get; } = new HashSet<Traitor>();
public TraitorMission Mission { get; internal set; }
public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat";
public virtual string InfoTextId { get; set; } = null;
public virtual string CompletedTextId { get; set; } = null;
public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress";
public virtual IEnumerable<string> StatusTextKeys => new [] { "[infotext]", "[status]" };
public virtual IEnumerable<string> StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) };
public virtual IEnumerable<string> InfoTextKeys => new string[] { };
public virtual IEnumerable<string> InfoTextValues(Traitor traitor) => new string[] { };
public virtual IEnumerable<string> CompletedTextKeys => new string[] { };
public virtual IEnumerable<string> CompletedTextValues(Traitor traitor) => new string[] { };
protected virtual string FormatText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
=> TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, textId, keys.Zip(values, (k,v) => (k,v)).ToArray());
protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
public virtual string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor));
public virtual string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor));
public virtual string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor);
public abstract bool IsCompleted { get; }
public virtual bool IsStarted(Traitor traitor) => Traitors.Contains(traitor);
public virtual bool CanBeCompleted(ICollection<Traitor> traitors) => !Traitors.Any(traitor => traitor.Character?.IsDead ?? true);
public virtual bool IsEnemy(Character character) => false;
public virtual bool IsAllowedToDamage(Structure structure) => false;
public virtual bool Start(Traitor traitor)
{
Traitors.Add(traitor);
return true;
}
public virtual void Update(float deltaTime)
{
}
protected Goal()
{
}
}
}
}
@@ -1,107 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalDestroyItemsWithTag : Goal
{
private readonly string tag;
private readonly bool matchIdentifier;
private readonly bool matchTag;
private readonly bool matchInventory;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" });
private readonly float destroyPercent;
private float DestroyPercent => destroyPercent;
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private int totalCount = 0;
private int targetCount = 0;
private string tagPrefabName = null;
private int CountMatchingItems()
{
int result = 0;
foreach (var item in Item.ItemList)
{
if (!matchInventory && Traitors.All(traitor => item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != traitor.Character) != null))
{
continue;
}
if (item.Submarine == null)
{
//items outside the sub don't count as destroyed if they're still in the traitor's inventory
bool carriedByTraitor = Traitors.Any(traitor => item.IsOwnedBy(traitor.Character));
if (!carriedByTraitor) { continue; }
}
else
{
if (Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; }
}
if (item.Condition <= 0.0f)
{
continue;
}
var identifierMatches = matchIdentifier && ((MapEntity)item).Prefab.Identifier == tag;
if (identifierMatches && tagPrefabName == null)
{
var textId = item.Prefab.GetItemNameTextId();
tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name.Value;
}
if (identifierMatches || (matchTag && item.HasTag(tag)))
{
++result;
}
}
// Quick fix
if (tagPrefabName == null && matchIdentifier)
{
tagPrefabName = TextManager.FormatServerMessage($"entityname.{tag}");
}
return result;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = CountMatchingItems() <= targetCount;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
totalCount = CountMatchingItems();
if (totalCount <= 0)
{
return false;
}
targetCount = (int)((1.0f - destroyPercent) * totalCount - 0.5f);
return true;
}
public GoalDestroyItemsWithTag(string tag, float destroyPercent, bool matchTag, bool matchIdentifier, bool matchInventory) : base()
{
InfoTextId = "TraitorGoalDestroyItems";
this.tag = tag;
this.destroyPercent = destroyPercent;
this.matchTag = matchTag;
this.matchIdentifier = matchIdentifier;
this.matchInventory = matchInventory;
}
}
}
}
@@ -1,162 +0,0 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalEntityTransformation : Goal
{
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[catalystitem]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { catalystItemName });
private bool isCompleted;
public override bool IsCompleted => isCompleted;
private string catalystItemIdentifier, catalystItemName;
private Vector2 activeEntitySavedPosition;
private Entity activeEntity;
private int activeEntityIndex;
private const float gracePeriod = 1f;
private const float graceDistance = 200f;
private float graceTimer;
private double transformationTime;
private enum EntityTypes { Character, Item }
private Identifier[] entities;
private EntityTypes[] entityTypes;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = HasTransformed(deltaTime);
}
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
return graceTimer <= gracePeriod;
}
private bool HasTransformed(float deltaTime)
{
if (activeEntity != null && !activeEntity.Removed)
{
activeEntitySavedPosition = activeEntity.WorldPosition;
}
else
{
if (transformationTime == 0)
{
graceTimer = 0.0f;
activeEntityIndex++;
transformationTime = Timing.TotalTime;
}
graceTimer += deltaTime;
switch (entityTypes[activeEntityIndex])
{
case EntityTypes.Character:
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < transformationTime)
{
continue;
}
if (character.SpeciesName == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance)
{
activeEntity = character;
transformationTime = 0.0;
return activeEntityIndex == entities.Length - 1;
}
}
break;
case EntityTypes.Item:
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID) || item.SpawnTime + gracePeriod < transformationTime)
{
continue;
}
if (((MapEntity)item).Prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance)
{
activeEntity = item;
transformationTime = 0.0;
return activeEntityIndex == entities.Length - 1;
}
}
break;
}
}
return false;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
catalystItemName = TextManager.FormatServerMessage($"entityname.{catalystItemIdentifier}");
activeEntity = null;
activeEntityIndex = 0;
switch (entityTypes[activeEntityIndex])
{
case EntityTypes.Character:
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (character.SpeciesName == entities[activeEntityIndex])
{
activeEntity = character;
break;
}
}
break;
case EntityTypes.Item:
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (((MapEntity)item).Prefab.Identifier == entities[0])
{
activeEntity = item;
break;
}
}
break;
}
graceTimer = 0.0f;
return activeEntity != null;
}
public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base()
{
this.entities = entities.ToIdentifiers().ToArray();
this.entityTypes = new EntityTypes[entityTypes.Length];
for (int i = 0; i < this.entityTypes.Length; i++)
{
this.entityTypes[i] = (EntityTypes)Enum.Parse(typeof(EntityTypes), entityTypes[i], true);
}
this.catalystItemIdentifier = catalystItemIdentifier;
}
}
}
}
@@ -1,239 +0,0 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class GoalFindItem : HumanoidGoal
{
private readonly TraitorMission.CharacterFilter filter;
private readonly string identifier;
private readonly bool preferNew;
private readonly bool allowNew;
private readonly bool allowExisting;
private readonly HashSet<Identifier> allowedContainerIdentifiers = new HashSet<Identifier>();
private ItemPrefab targetPrefab;
private ItemPrefab containedPrefab;
private Item targetContainer;
private Item target;
private HashSet<Item> existingItems = new HashSet<Item>();
private string targetNameText;
private string targetContainerNameText;
private string targetHullNameText;
private float percentage;
private int spawnAmount = 1;
private const string itemContainerId = "toolbox";
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" });
public override bool IsCompleted => target != null && Traitors.Any(traitor => traitor.Character.HasItem(target));
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
if (!base.CanBeCompleted(traitors))
{
return false;
}
if (target == null)
{
var targetPrefabCandidate = FindItemPrefab(identifier);
return targetPrefabCandidate != null && FindTargetContainer(traitors, targetPrefabCandidate) != null;
}
if (target.Removed)
{
return false;
}
if (target.Submarine == null)
{
if (!(target.ParentInventory?.Owner is Character))
{
return false;
}
}
else
{
if (Traitors.All(traitor => target.Submarine.TeamID != traitor.Character.TeamID))
{
return false;
}
}
return true;
}
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null);
protected ItemPrefab FindItemPrefab(string identifier)
{
return (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier);
}
protected Item FindRandomContainer(ICollection<Traitor> traitors, ItemPrefab targetPrefabCandidate, bool includeNew, bool includeExisting)
{
List<Item> suitableItems = new List<Item>();
foreach (Item item in Item.ItemList)
{
if (item.HiddenInGame || item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
if (item.Submarine == null || traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID))
{
continue;
}
if (item.GetComponent<ItemContainer>() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier))
{
if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null))
{
suitableItems.Add(item);
}
}
}
if (suitableItems.Count == 0) { return null; }
return suitableItems[TraitorManager.RandomInt(suitableItems.Count)];
}
protected Item FindTargetContainer(ICollection<Traitor> traitors, ItemPrefab targetPrefabCandidate)
{
Item result = null;
if (preferNew)
{
result = FindRandomContainer(traitors, targetPrefabCandidate, true, false);
}
if (result == null)
{
result = FindRandomContainer(traitors, targetPrefabCandidate, allowNew, allowExisting);
}
if (result == null)
{
return null;
}
if (allowNew && !result.OwnInventory.IsFull())
{
return result;
}
if (allowExisting && result.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)
{
return result;
}
return null;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (targetPrefab != null)
{
return true;
}
string targetPrefabTextId;
if (percentage > 0f)
{
spawnAmount = (int)Math.Floor(Character.CharacterList.FindAll(c => c.TeamID == traitor.Character.TeamID && c != traitor.Character && !c.IsDead && (filter == null || filter(c))).Count * percentage);
}
if (spawnAmount > 1 && allowNew)
{
containedPrefab = FindItemPrefab(identifier);
targetPrefab = FindItemPrefab(itemContainerId);
if (containedPrefab == null || targetPrefab == null)
{
return false;
}
targetPrefabTextId = containedPrefab.GetItemNameTextId();
}
else
{
spawnAmount = 1;
containedPrefab = null;
targetPrefab = FindItemPrefab(identifier);
if (targetPrefab == null)
{
return false;
}
targetPrefabTextId = targetPrefab.GetItemNameTextId();
}
targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name.Value;
targetContainer = FindTargetContainer(Traitors, targetPrefab);
if (targetContainer == null)
{
targetPrefab = null;
targetContainer = null;
return false;
}
var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId();
targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name.Value;
var targetHullTextId = targetContainer.CurrentHull?.Prefab.GetHullNameTextId();
targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName.Value ?? "";
if (allowNew && !targetContainer.OwnInventory.IsFull())
{
existingItems.Clear();
foreach (var item in targetContainer.OwnInventory.AllItems.Distinct())
{
existingItems.Add(item);
}
Entity.Spawner.AddItemToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item =>
{
item.AddTag("traitormissionitem");
});
target = null;
}
else if (allowExisting)
{
target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier);
}
else
{
targetPrefab = null;
targetContainer = null;
return false;
}
return true;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (target == null)
{
target = targetContainer.ContainedItems.FirstOrDefault(item => item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item));
if (target != null)
{
if (containedPrefab != null)
{
for (int i = 0; i < spawnAmount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(containedPrefab, target.OwnInventory);
}
}
existingItems.Clear();
}
}
}
public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params Identifier[] allowedContainerIdentifiers)
{
this.filter = filter;
this.identifier = identifier;
this.preferNew = preferNew;
this.allowNew = allowNew;
this.allowExisting = allowExisting;
this.percentage = percentage / 100f;
this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers);
}
}
}
}
@@ -1,46 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalFloodPercentOfSub : Goal
{
private readonly float minimumFloodingAmount;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
var validHullsCount = 0;
var floodingAmount = 0.0f;
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; }
if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; }
++validHullsCount;
floodingAmount += hull.WaterVolume / hull.Volume;
}
if (validHullsCount > 0)
{
floodingAmount /= validHullsCount;
}
isCompleted = floodingAmount >= minimumFloodingAmount;
}
public GoalFloodPercentOfSub(float minimumFloodingAmount) : base()
{
InfoTextId = "TraitorGoalFloodPercentOfSub";
this.minimumFloodingAmount = minimumFloodingAmount;
}
}
}
}
@@ -1,84 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalInjectTarget : Goal
{
public TraitorMission.CharacterFilter Filter { get; private set; }
public List<Character> Targets { get; private set; }
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[poison]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", poisonName });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character));
private string poisonId;
private string afflictionId;
private string poisonName;
private int targetCount;
private float targetPercentage;
private bool[] targetWasInfected;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = WereAllTargetsInfected();
}
private bool WereAllTargetsInfected()
{
if (targetWasInfected == null) { return false; }
for (int i = 0; i < targetWasInfected.Length; i++)
{
if (targetWasInfected[i]) continue;
targetWasInfected[i] = Targets[i].CharacterHealth.GetAffliction(afflictionId) != null;
}
return targetWasInfected.All(t => t == true);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId;
Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage);
if (Targets == null)
{
return false;
}
targetWasInfected = new bool[Targets.Count];
return !Targets.All(t => t.IsDead);
}
public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base()
{
Filter = filter;
this.poisonId = poisonId;
this.afflictionId = afflictionId;
this.targetCount = targetCount;
this.targetPercentage = targetPercentage / 100f;
if (this.targetPercentage < 1.0f)
{
InfoTextId = "traitorgoalpoisoninfo";
}
else
{
InfoTextId = "traitorgoalpoisoneveryoneinfo";
}
}
}
}
}
@@ -1,73 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalKeepTransformedAlive : Goal
{
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[speciesname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetCharacterName });
public override bool IsCompleted => isCompleted;
private bool isCompleted;
private const float gracePeriod = 1f;
private Identifier speciesId;
private string targetCharacterName;
private Character targetCharacter;
private float timer;
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
return timer < gracePeriod || targetCharacter != null && !targetCharacter.IsDead;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (timer <= gracePeriod)
{
timer += deltaTime;
}
isCompleted = targetCharacter != null && !targetCharacter.IsDead && timer >= gracePeriod;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
var startTime = Timing.TotalTime;
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < startTime)
{
continue;
}
if (character.SpeciesName == speciesId)
{
targetCharacter = character;
break;
}
}
targetCharacterName = TextManager.FormatServerMessage($"character.{speciesId}").ToLowerInvariant();
return targetCharacter != null;
}
public GoalKeepTransformedAlive(Identifier speciesId) : base()
{
this.speciesId = speciesId;
}
}
}
}
@@ -1,163 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalKillTarget : Goal
{
public TraitorMission.CharacterFilter Filter { get; private set; }
public List<Character> Targets { get; private set; }
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[]
{ traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath().Value, targetHull != null ? TextManager.Get($"roomname.{targetHull}").Value : string.Empty });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character));
private CauseOfDeathType requiredCauseOfDeath;
private string afflictionId;
private string targetHull;
private int targetCount;
private float targetPercentage;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = DoesDeathMatchCriteria();
}
private bool DoesDeathMatchCriteria()
{
if (Targets == null || Targets.Any(t => !t.IsDead)) return false;
bool typeMatch = false;
for (int i = 0; i < Targets.Count; i++)
{
// No specified cause of death required or missing cause of death
if (requiredCauseOfDeath == CauseOfDeathType.Unknown || Targets[i].CauseOfDeath == null)
{
typeMatch = true;
}
else
{
switch (Targets[i].CauseOfDeath.Type)
{
// If a cause of death is labeled as unknown, side with the traitor and accept this regardless of the required type
case CauseOfDeathType.Unknown:
typeMatch = true;
break;
case CauseOfDeathType.Pressure:
case CauseOfDeathType.Suffocation:
case CauseOfDeathType.Drowning:
typeMatch = requiredCauseOfDeath == Targets[i].CauseOfDeath.Type;
break;
case CauseOfDeathType.Affliction:
typeMatch = Targets[i].CauseOfDeath.Type == requiredCauseOfDeath && Targets[i].CauseOfDeath.Affliction.Identifier == afflictionId;
break;
case CauseOfDeathType.Disconnected:
typeMatch = false;
break;
}
}
if (targetHull != null)
{
if (Targets[i].CurrentHull != null)
{
if (typeMatch && Targets[i].CurrentHull.RoomName == targetHull || Targets[i].CurrentHull.RoomName.Contains(targetHull))
{
continue;
}
else
{
return false;
}
}
else
{
// Outside the submarine, not supported for now
return false;
}
}
else
{
if (typeMatch)
{
continue;
}
else
{
return false;
}
}
}
return true;
}
private LocalizedString GetCauseOfDeath()
{
if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty)
{
return requiredCauseOfDeath.ToString().ToLower();
}
else
{
return TextManager.Get($"afflictionname.{afflictionId}").ToLower();
}
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage);
return Targets != null && !Targets.All(t => t.IsDead);
}
public GoalKillTarget(TraitorMission.CharacterFilter filter, CauseOfDeathType requiredCauseOfDeath, string afflictionId, string targetHull, int targetCount, float targetPercentage) : base()
{
Filter = filter;
this.requiredCauseOfDeath = requiredCauseOfDeath;
this.afflictionId = afflictionId;
this.targetHull = targetHull;
this.targetCount = targetCount;
this.targetPercentage = targetPercentage / 100f;
if (this.targetPercentage < 1f)
{
if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull == null)
{
InfoTextId = "traitorgoalkilltargetinfo";
}
else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull == null)
{
InfoTextId = "traitorgoalkilltargetinfowithcause";
}
else if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull != null)
{
InfoTextId = "traitorgoalkilltargetinfowithhull";
}
else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull != null)
{
InfoTextId = "traitorgoalkilltargetinfowithcauseandhull";
}
}
else
{
InfoTextId = "traitorgoalkilleveryoneinfo";
}
}
}
}
}
@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalReachDistanceFromSub : Goal
{
private readonly float requiredDistance;
private readonly float requiredDistanceSqr;
private float requiredDistanceInMeters;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistanceInMeters:0.00}" });
public override bool IsCompleted
{
get
{
return Traitors.Any(traitor =>
{
Submarine ownSub = null;
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] != null && Submarine.MainSubs[i].TeamID == traitor.Character.TeamID)
{
ownSub = Submarine.MainSubs[i];
break;
}
}
if (ownSub == null) return false;
var characterPosition = traitor.Character.WorldPosition;
var submarinePosition = ownSub.WorldPosition;
var distance = Vector2.DistanceSquared(characterPosition, submarinePosition);
return distance >= requiredDistanceSqr;
});
}
}
public GoalReachDistanceFromSub(float requiredDistance) : base()
{
InfoTextId = "TraitorGoalReachDistanceFromSub";
requiredDistanceInMeters = requiredDistance;
this.requiredDistance = requiredDistance / Physics.DisplayToRealWorldRatio;
requiredDistanceSqr = this.requiredDistance * this.requiredDistance;
}
}
}
}
@@ -1,70 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class GoalReplaceInventory : HumanoidGoal
{
private readonly HashSet<Identifier> sabotageContainerIds = new HashSet<Identifier>();
private readonly HashSet<Identifier> validReplacementIds = new HashSet<Identifier>();
private readonly float replaceAmount;
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override IEnumerable<string> StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" });
public override IEnumerable<string> StatusTextValues(Traitor traitor) => base.StatusTextValues(traitor).Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) });
public override void Update(float deltaTime)
{
base.Update(deltaTime);
int totalAmount = 0, replacedAmount = 0;
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID))
{
continue;
}
if (item.FindParentInventory(inventory => inventory.Owner is Character) != null)
{
continue;
}
if (sabotageContainerIds.Contains(((MapEntity)item).Prefab.Identifier))
{
++totalAmount;
if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier)))
{
continue;
}
++replacedAmount;
}
}
isCompleted = replacedAmount >= (int)(replaceAmount * totalAmount + 0.5f);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (sabotageContainerIds.Count <= 0 || validReplacementIds.Count <= 0)
{
return false;
}
return true;
}
public GoalReplaceInventory(Identifier[] containerIds, Identifier[] replacementIds, float replaceAmount)
{
sabotageContainerIds.UnionWith(containerIds);
validReplacementIds.UnionWith(replacementIds);
this.replaceAmount = replaceAmount;
}
}
}
}
@@ -1,62 +0,0 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalSabotageItems : HumanoidGoal
{
private readonly string tag;
private readonly float conditionThreshold;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private readonly List<Item> targetItems = new List<Item>();
private string targetItemPrefabName = null;
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (item.Condition > conditionThreshold && (item.Prefab?.Identifier == tag || item.HasTag(tag)))
{
targetItems.Add(item);
}
}
if (targetItems.Count > 0)
{
var textId = targetItems[0].Prefab.GetItemNameTextId();
targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name.Value;
}
return targetItems.Count > 0;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = targetItems.All(item => item.Condition <= conditionThreshold);
}
public GoalSabotageItems(string tag, float conditionThreshold) : base()
{
this.tag = tag;
this.conditionThreshold = conditionThreshold;
InfoTextId = "TraitorGoalSabotageInfo";
}
}
}
}
@@ -1,96 +0,0 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalUnwiring : HumanoidGoal
{
private readonly string tag;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[connectionname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetItemPrefabName ?? "", targetConnectionDisplayName ?? targetConnectionName });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private readonly List<ConnectionPanel> targetConnectionPanels = new List<ConnectionPanel>();
private string targetItemPrefabName;
private string targetConnectionName;
private string targetConnectionDisplayName;
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (item.Prefab?.Identifier == tag || item.HasTag(tag))
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null)
{
targetConnectionPanels.Add(connectionPanel);
}
}
}
if (targetConnectionPanels.Count > 0)
{
var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId();
targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name.Value;
}
return targetConnectionPanels.Count > 0;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = AreTargetsUnwired();
}
private bool AreTargetsUnwired()
{
for (int i = 0; i < targetConnectionPanels.Count; i++)
{
for (int j = 0; j < targetConnectionPanels[i].Connections.Count; j++)
{
if (targetConnectionPanels[i].Connections[j] == null || targetConnectionPanels[i].Connections[j].Wires == null) continue;
if (targetConnectionName != string.Empty)
{
if (targetConnectionPanels[i].Connections[j].Name != targetConnectionName) continue;
}
if (!targetConnectionPanels[i].Connections[j].Wires.All(w => w == null)) return false;
}
}
return true;
}
public GoalUnwiring(string tag, string targetConnectionName, string targetConnectionDisplayTag) : base()
{
this.tag = tag;
this.targetConnectionName = targetConnectionName;
if (targetConnectionDisplayTag != string.Empty)
{
targetConnectionDisplayName = TextManager.FormatServerMessage(targetConnectionDisplayTag);
InfoTextId = "TraitorGoalUnwireInfo";
}
else
{
InfoTextId = "TraitorGoalUnwireAllInfo";
}
}
}
}
}
@@ -1,35 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalWaitForTraitors : Goal
{
private readonly int requiredCount;
private int count = 0;
public override bool IsCompleted => count >= requiredCount;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[remaining]", "[count]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredCount - count}", $"{requiredCount}" });
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
++count;
return true;
}
public GoalWaitForTraitors(int requiredCount) : base()
{
this.requiredCount = requiredCount;
InfoTextId = "TraitorGoalWaitForTraitorsInfoText";
}
}
}
}
@@ -1,17 +0,0 @@
namespace Barotrauma
{
partial class Traitor
{
public abstract class HumanoidGoal : Goal
{
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
return traitor?.Character?.IsHumanoid ?? false;
}
}
}
}
@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalHasDuration : Modifier
{
private readonly float requiredDuration;
private readonly bool countTotalDuration;
private readonly string durationInfoTextId;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString(CultureInfo.InvariantCulture) });
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId,
("[infotext]", infoText), ("[duration]", requiredDuration.ToString(CultureInfo.InvariantCulture))) : infoText;
}
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private float remainingDuration = float.NaN;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (Goal.IsCompleted)
{
if (!float.IsNaN(remainingDuration))
{
remainingDuration -= deltaTime;
}
else
{
remainingDuration = requiredDuration;
}
isCompleted |= remainingDuration <= 0.0f;
}
else if (!countTotalDuration)
{
remainingDuration = float.NaN;
}
}
public GoalHasDuration(Goal goal, float requiredDuration, bool countTotalDuration, string durationInfoTextId) : base(goal)
{
this.requiredDuration = requiredDuration;
this.countTotalDuration = countTotalDuration;
this.durationInfoTextId = durationInfoTextId;
}
}
}
}
@@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalHasTimeLimit : Modifier
{
private readonly float timeLimit;
private readonly string timeLimitInfoTextId;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" });
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, ("[infotext]", infoText), ("[timelimit]", $"{TimeSpan.FromSeconds(timeLimit):g}")) : infoText;
}
public override bool CanBeCompleted(ICollection<Traitor> traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f);
private float timeRemaining;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
timeRemaining = System.Math.Max(0.0f, timeRemaining - deltaTime);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
timeRemaining = timeLimit;
return true;
}
public GoalHasTimeLimit(Goal goal, float timeLimit, string timeLimitInfoTextId) : base(goal)
{
this.timeLimit = timeLimit;
this.timeLimitInfoTextId = timeLimitInfoTextId;
}
}
}
}
@@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalIsOptional : Modifier
{
private readonly string optionalInfoTextId;
public override string StatusValueTextId => (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)) ? "failed" : base.StatusValueTextId;
public override IEnumerable<string> StatusTextValues(Traitor traitor)
{
var values = base.StatusTextValues(traitor).ToArray();
values[1] = TextManager.FormatServerMessage(StatusValueTextId);
return values;
}
public override bool IsCompleted => base.IsCompleted || (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors));
public override bool CanBeCompleted(ICollection<Traitor> traitors) => true;
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, ("[infotext]", infoText)) : infoText;
}
public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal)
{
this.optionalInfoTextId = optionalInfoTextId;
}
}
}
}
@@ -1,80 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public abstract class Modifier : Goal
{
protected Goal Goal { get; }
public override string StatusValueTextId => Goal.StatusValueTextId;
public override string StatusTextId
{
get => Goal.StatusTextId;
set => Goal.StatusTextId = value;
}
public override string InfoTextId
{
get => Goal.InfoTextId;
set => Goal.InfoTextId = value;
}
public override string CompletedTextId
{
get => Goal.CompletedTextId;
set => Goal.CompletedTextId = value;
}
public override IEnumerable<string> StatusTextKeys => Goal.StatusTextKeys;
public override IEnumerable<string> StatusTextValues(Traitor traitor) => new string[] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) };
public override IEnumerable<string> InfoTextKeys => Goal.InfoTextKeys;
public override IEnumerable<string> InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor);
public override IEnumerable<string> CompletedTextKeys => Goal.CompletedTextKeys;
public override IEnumerable<string> CompletedTextValues(Traitor traitor) => Goal.CompletedTextValues(traitor);
protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetStatusText(traitor, textId, keys, values);
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetInfoText(traitor, textId, keys, values);
protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetCompletedText(traitor, textId, keys, values);
public override string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor));
public override string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor));
public override string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor);
public override bool IsCompleted => Goal.IsCompleted;
public override bool IsStarted(Traitor traitor) => base.IsStarted(traitor) && Goal.IsStarted(traitor);
public override bool CanBeCompleted(ICollection<Traitor> traitors) => base.CanBeCompleted(traitors) && Goal.CanBeCompleted(traitors);
public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character);
public override void Update(float deltaTime)
{
base.Update(deltaTime);
Goal.Update(deltaTime);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (!Goal.Start(traitor))
{
return false;
}
return true;
}
protected Modifier(Goal goal) : base()
{
Goal = goal;
}
}
}
}
@@ -1,194 +0,0 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class Objective
{
public Traitor Traitor { get; private set; }
private int shuffleGoalsCount;
private readonly List<Goal> allGoals = new List<Goal>();
private readonly List<Goal> activeGoals = new List<Goal>();
private readonly List<Goal> pendingGoals = new List<Goal>();
private readonly List<Goal> completedGoals = new List<Goal>();
public bool IsCompleted => pendingGoals.Count <= 0;
public bool IsPartiallyCompleted => completedGoals.Count > 0;
public bool IsStarted { get; private set; } = false;
public bool CanBeStarted(ICollection<Traitor> traitors) => !IsStarted && allGoals.Any(goal => goal.CanBeCompleted(traitors));
public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted(goal.Traitors));
public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character));
public bool IsAllowedToDamage(Structure structure) => pendingGoals.Any(goal => goal.IsAllowedToDamage(structure));
public readonly HashSet<string> Roles = new HashSet<string>();
public string InfoText { get; private set; }
public virtual string GoalInfoFormatId { get; set; } = "TraitorObjectiveGoalInfoFormat";
public string GoalInfos =>
string.Join("/",
string.Join("/", activeGoals.Select((goal, index) =>
{
var statusText = goal.StatusText(Traitor);
var startIndex = statusText.LastIndexOf('/') + 1;
return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}";
}).ToArray()),
string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray()));
public string AllGoalInfos =>
string.Join("/",
string.Join("/", allGoals.Select((goal, index) =>
{
var statusText = goal.StatusText(Traitor);
var startIndex = statusText.LastIndexOf('/') + 1;
return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}";
}).ToArray()),
string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray()));
public virtual string StartMessageTextId { get; set; } = "TraitorObjectiveStartMessage";
public virtual IEnumerable<string> StartMessageKeys => new string[] { "[traitorgoalinfos]" };
public virtual IEnumerable<string> StartMessageValues => new string[] { GoalInfos };
public virtual LocalizedString StartMessageText
=> TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageTextId, StartMessageKeys.Zip(StartMessageValues, (k,v) => (k,v)).ToArray());
public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer";
public virtual IEnumerable<string> StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" });
public virtual IEnumerable<string> StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" });
public virtual LocalizedString StartMessageServerText => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageServerTextId, StartMessageServerKeys.Zip(StartMessageServerValues, (k,v) => (k,v)).ToArray());
public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess";
public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead";
public virtual string EndMessageSuccessDetainedTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDetained";
public virtual string EndMessageFailureTextId { get; set; } = "TraitorObjectiveEndMessageFailure";
public virtual string EndMessageFailureDeadTextId { get; set; } = "TraitorObjectiveEndMessageFailureDead";
public virtual string EndMessageFailureDetainedTextId { get; set; } = "TraitorObjectiveEndMessageFailureDetained";
public virtual IEnumerable<string> EndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" };
public virtual IEnumerable<string> EndMessageValues => new string[] { Traitor?.Character?.Name ?? "(unknown)", GoalInfos };
public virtual string EndMessageText
{
get
{
var traitorIsDead = Traitor.Character.IsDead;
var traitorIsDetained = Traitor.Character.LockHands;
var messageId = IsCompleted
? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId)
: (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId);
return TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, messageId, EndMessageKeys.Zip(EndMessageValues, (k,v)=>(k,v)).ToArray());
}
}
public bool Start(Traitor traitor)
{
Traitor = traitor;
activeGoals.Clear();
pendingGoals.Clear();
completedGoals.Clear();
var allGoalsCount = allGoals.Count;
var indices = allGoals.Select((goal, index) => index).ToArray();
if (shuffleGoalsCount > 0)
{
for (var i = allGoalsCount; i > 1;)
{
int j = TraitorManager.RandomInt(i--);
var temp = indices[j];
indices[j] = indices[i];
indices[i] = temp;
}
}
for (var i = 0; i < allGoalsCount; ++i)
{
var goal = allGoals[indices[i]];
if (goal.Start(traitor))
{
activeGoals.Add(goal);
pendingGoals.Add(goal);
if (shuffleGoalsCount > 0 && pendingGoals.Count >= shuffleGoalsCount)
{
break;
}
}
else
{
completedGoals.Add(goal);
}
}
if (pendingGoals.Count <= 0 && completedGoals.Count < allGoals.Count)
{
return false;
}
IsStarted = true;
traitor.SendChatMessageBox(StartMessageText.Value, traitor.Mission.Identifier);
traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission.Identifier);
return true;
}
public void StartMessage()
{
Traitor.SendChatMessage(StartMessageText.Value, Traitor.Mission.Identifier);
}
public void EndMessage()
{
Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission.Identifier);
Traitor.SendChatMessage(EndMessageText, Traitor.Mission.Identifier);
}
public void Update(float deltaTime)
{
if (!IsStarted)
{
return;
}
for (int i = 0; i < pendingGoals.Count;)
{
var goal = pendingGoals[i];
goal.Update(deltaTime);
if (!goal.IsCompleted)
{
++i;
}
else
{
completedGoals.Add(goal);
pendingGoals.RemoveAt(i);
if (GameMain.Server != null)
{
Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission.Identifier);
if (pendingGoals.Count > 0)
{
Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission.Identifier);
}
Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission.Identifier);
}
}
}
}
public Objective(string infoText, int shuffleGoalsCount, ICollection<string> roles, ICollection<Goal> goals)
{
InfoText = infoText;
this.shuffleGoalsCount = shuffleGoalsCount;
Roles.UnionWith(roles);
allGoals.AddRange(goals);
}
}
}
}
@@ -1,43 +0,0 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Traitor
{
public readonly Character Character;
public string Role { get; }
public TraitorMission Mission { get; }
public Objective CurrentObjective => Mission.GetCurrentObjective(this);
public Traitor(TraitorMission mission, string role, Character character)
{
Mission = mission;
Role = role;
Character = character;
Character.IsTraitor = true;
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData());
}
public delegate void MessageSender(string message);
public void SendChatMessage(string serverText, Identifier iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server);
}
public void SendChatMessageBox(string serverText, Identifier iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox);
}
public void UpdateCurrentObjective(string objectiveText, Identifier iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
Character.TraitorCurrentObjective = objectiveText;
GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective.Value, iconIdentifier, TraitorMessageType.Objective);
}
}
}
@@ -1,207 +1,505 @@
// #define DISABLE_MISSIONS
#nullable enable
using System;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class TraitorManager
{
public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks);
sealed partial class TraitorManager
{
const int MaxPreviousEventHistory = 10;
// All traitor related functionality should use the following interface for generating random values
public static int RandomInt(int n) => Random.Next(n);
const int StartDelayMin = 60;
const int StartDelayMax = 200;
// All traitor related functionality should use the following interface for generating random values
public static double RandomDouble() => Random.NextDouble();
private float startTimer;
private bool started = false;
public readonly Dictionary<CharacterTeamType, Traitor.TraitorMission> Missions = new Dictionary<CharacterTeamType, Traitor.TraitorMission>();
private TraitorResults? results = null;
public string GetCodeWords(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : "";
public string GetCodeResponse(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : "";
public IEnumerable<Traitor> Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values);
private float startCountdown = 0.0f;
private GameServer server;
public bool ShouldEndRound
public class PreviousTraitorEvent
{
get;
set;
public TraitorEventPrefab TraitorEvent { get; }
public TraitorEvent.State State { get; }
public Client Traitor =>
GameMain.Server.ConnectedClients.Find(c => traitorAccountId.IsSome() && traitorAccountId == c.AccountId) ??
GameMain.Server.ConnectedClients.Find(c => traitorAddress == c.Connection.Endpoint.Address);
private readonly Address traitorAddress;
private readonly Option<AccountId> traitorAccountId;
public PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Client traitor)
{
TraitorEvent = traitorEvent;
State = state;
traitorAddress = traitor.Connection.Endpoint.Address;
traitorAccountId = traitor.AccountId;
}
private PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Option<AccountId> accountId, Address address)
{
TraitorEvent = traitorEvent;
State = state;
traitorAddress = address;
traitorAccountId = accountId;
}
public void Save(XElement parentElement)
{
parentElement.Add(
new XElement(nameof(PreviousTraitorEvent),
new XAttribute("id", TraitorEvent.Identifier),
new XAttribute("state", State),
new XAttribute("accountid", traitorAccountId),
new XAttribute("address", traitorAddress)));
}
public static PreviousTraitorEvent? Load(XElement subElement)
{
var traitorEventId = subElement.GetAttributeIdentifier("id", Identifier.Empty);
var state = subElement.GetAttributeEnum("state", Barotrauma.TraitorEvent.State.Failed);
var accountId = Networking.AccountId.Parse(
subElement.GetAttributeString("accountid", null)
?? subElement.GetAttributeString("steamid", ""));
var address = Address.Parse(
subElement.GetAttributeString("address", null)
?? subElement.GetAttributeString("endpoint", ""))
.Fallback(new UnknownAddress());
if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab? prefab) && prefab is TraitorEventPrefab traitorEventPrefab)
{
return new PreviousTraitorEvent(traitorEventPrefab, state, accountId, address);
}
else
{
DebugConsole.ThrowError($"Error when loading {nameof(TraitorManager)}: could not find a traitor event prefab with the identifier \"{traitorEventId}\".");
return null;
}
}
}
public readonly record struct ActiveTraitorEvent(
Client Traitor,
TraitorEvent TraitorEvent);
private readonly List<PreviousTraitorEvent> previousTraitorEvents = new List<PreviousTraitorEvent>();
private readonly List<ActiveTraitorEvent> activeEvents = new List<ActiveTraitorEvent>();
public IEnumerable<ActiveTraitorEvent> ActiveEvents => activeEvents;
private readonly GameServer server;
private EventManager? eventManager;
private Level? level;
public bool Enabled;
public bool IsTraitor(Character character)
{
if (Traitors == null)
return activeEvents.Any(e => e.Traitor.Character == character);
}
public TraitorManager(GameServer server)
{
this.server = server;
}
public void Initialize(EventManager eventManager, Level level)
{
this.eventManager = eventManager;
this.level = level;
startTimer = Rand.Range(StartDelayMin, StartDelayMax);
started = false;
results = null;
}
private bool TryCreateTraitorEvents(EventManager eventManager, Level level)
{
var eventPrefabs = EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Cast<TraitorEventPrefab>();
if (!eventPrefabs.Any())
{
DebugConsole.AddWarning("No traitor event available in any of the enabled content packages.");
return false;
}
return Traitors.Any(traitor => traitor.Character == character);
if (server.ConnectedClients.Count(IsClientViableTraitor) < server.ServerSettings.TraitorsMinPlayerCount)
{
DebugConsole.AddWarning("Not enough clients to create a traitor event. Active traitor events: " + activeEvents.Count);
#if DEBUG
DebugConsole.AddWarning("Starting a traitor event anyway because this is a debug build.");
#else
return false;
#endif
}
int maxDangerLevel = server.ServerSettings.TraitorDangerLevel;
int playerCount = server.ConnectedClients.Count(c => c.Character != null && !c.Character.Removed);
var campaign = GameMain.GameSession?.Campaign;
var suitablePrefabs = eventPrefabs
.Where(e => EventManager.IsLevelSuitable(e, level))
.Where(e => e.ReputationRequirementsMet(campaign))
.Where(e => e.MissionRequirementsMet(GameMain.GameSession))
.Where(e => e.LevelRequirementsMet(level))
.Where(e => e.DangerLevel <= maxDangerLevel)
.Where(e => playerCount >= e.MinPlayerCount)
.ToList();
var minDangerLevelPrefabs = suitablePrefabs.FindAll(e => e.DangerLevel == TraitorEventPrefab.MinDangerLevel);
if (!suitablePrefabs.Any())
{
//this is normal, there e.g. might be no missions for an abandoned outpost or end level
DebugConsole.Log("No suitable traitor missions available for the level.");
return false;
}
foreach (var previousEvent in previousTraitorEvents.DistinctBy(e => e.Traitor).Reverse())
{
if (previousEvent.State == TraitorEvent.State.Completed &&
previousEvent.TraitorEvent.DangerLevel < maxDangerLevel &&
previousEvent.TraitorEvent.IsChainable &&
IsClientViableTraitor(previousEvent.Traitor))
{
GameServer.Log($"{NetworkMember.ClientLogName(previousEvent.Traitor)} successfully completed a traitor event on a previous round. Attempting to give choose them a new, more dangerous event...", ServerLog.MessageType.Traitors);
var suitablePrefab =
//try finding an event that's continuation from the previous one (= requires the previous one to be completed 1st)
suitablePrefabs.GetRandomUnsynced(p => p.RequiredCompletedTags.Any(t =>
previousEvent.TraitorEvent.Identifier == t || previousEvent.TraitorEvent.Tags.Contains(t)));
suitablePrefab ??=
//otherwise try finding some higher-difficult event for the same faction
suitablePrefabs.GetRandomUnsynced(p =>
p.RequiredCompletedTags.None() &&
p.DangerLevel > previousEvent.TraitorEvent.DangerLevel &&
p.Faction == previousEvent.TraitorEvent.Faction);
if (suitablePrefab != null)
{
CreateTraitorEvent(eventManager, suitablePrefab, previousEvent.Traitor);
return true;
}
else
{
GameServer.Log($"Could not find a suitable, more difficult traitor event for {NetworkMember.ClientLogName(previousEvent.Traitor)}.", ServerLog.MessageType.Traitors);
}
}
}
TraitorEventPrefab selectedPrefab;
if (GameMain.GameSession?.Campaign == null)
{
selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced);
}
else
{
selectedPrefab = minDangerLevelPrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced);
if (selectedPrefab == null)
{
GameServer.Log($"Could not find a suitable danger level {TraitorEventPrefab.MinDangerLevel} traitor event. Choosing a random event instead.", ServerLog.MessageType.Traitors);
selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced);
}
}
if (selectedPrefab != null)
{
var selectedTraitor = SelectRandomTraitor();
if (selectedTraitor == null)
{
DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\".");
return false;
}
CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor);
return true;
}
return false;
}
public string GetTraitorRole(Character character)
private Client? SelectRandomTraitor()
{
var traitor = Traitors.FirstOrDefault(candidate => candidate.Character == character);
if (GameSettings.CurrentConfig.VerboseLogging)
{
GameServer.Log(
$"Choosing a random traitor... Available traitors:"
+ string.Concat(server.ConnectedClients.Where(IsClientViableTraitor).Select(c => $"\n - {c.Name} ({(int)(GetTraitorProbability(c) * 100)}%)")),
ServerLog.MessageType.Traitors);
}
return server.ConnectedClients.Where(IsClientViableTraitor).GetRandomByWeight(GetTraitorProbability, Rand.RandSync.Unsynced);
}
private IEnumerable<Client> SelectSecondaryTraitors(TraitorEvent traitorEvent, Client mainTraitor)
{
if (traitorEvent.Prefab.SecondaryTraitorPercentage <= 0.0f &&
traitorEvent.Prefab.SecondaryTraitorAmount <= 0)
{
return Enumerable.Empty<Client>();
}
var viableTraitors = server.ConnectedClients.Where(c => c != mainTraitor && IsClientViableTraitor(c)).ToList();
int amountToChoose = (int)Math.Ceiling(viableTraitors.Count * (traitorEvent.Prefab.SecondaryTraitorPercentage / 100.0f));
amountToChoose = Math.Max(amountToChoose, traitorEvent.Prefab.SecondaryTraitorAmount);
if (amountToChoose > viableTraitors.Count)
{
DebugConsole.ThrowError(
$"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+
$"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.");
amountToChoose = viableTraitors.Count;
}
List<Client> traitors = new List<Client>();
for (int i = 0; i < amountToChoose; i++)
{
var traitor = viableTraitors.GetRandomUnsynced();
viableTraitors.Remove(traitor);
traitors.Add(traitor);
}
return traitors;
}
private bool IsClientViableTraitor(Client client)
{
return
client != null &&
server.ConnectedClients.Contains(client) &&
client.Character != null &&
!client.Character.IsIncapacitated && !client.Character.Removed &&
activeEvents.None(e => e.Traitor == client);
}
private float GetTraitorEventPrefabCommonness(TraitorEventPrefab prefab)
{
int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.TraitorEvent == prefab);
if (roundsSinceLastSelected.HasValue)
{
float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value);
//exponentially decreasing commonness the closer the last time the event was selected
return prefab.Commonness * normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected;
}
else
{
return prefab.Commonness;
}
}
private float GetTraitorProbability(Client client)
{
int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.Traitor == client);
if (roundsSinceLastSelected.HasValue)
{
float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value);
//exponentially decreasing commonness the closer the last time the event was selected
return normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected;
}
else
{
return 1.0f;
}
}
private int? GetRoundsSinceLastSelected(Func<PreviousTraitorEvent, bool> condition)
{
//most recent events are at the end of the list, start from there
for (int i = previousTraitorEvents.Count - 1; i >= 0; i--)
{
if (condition(previousTraitorEvents[i]))
{
return previousTraitorEvents.Count - i;
}
}
return null;
}
private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor)
{
if (selectedPrefab.TryCreateInstance<TraitorEvent>(out var newEvent))
{
var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor);
string logMessage = $"{NetworkMember.ClientLogName(traitor)} was selected as a traitor. Selected event: {selectedPrefab.Name}";
if (secondaryTraitors.Any())
{
logMessage += $", secondary traitors: {string.Join(", ", secondaryTraitors.Select(c => NetworkMember.ClientLogName(c)))}";
}
GameServer.Log(logMessage, ServerLog.MessageType.Traitors);
newEvent.OnStateChanged += () => SendCurrentState(newEvent);
activeEvents.Add(new ActiveTraitorEvent(traitor, newEvent));
newEvent.SetTraitor(traitor);
newEvent.SetSecondaryTraitors(secondaryTraitors);
eventManager.ActivateEvent(newEvent);
SendCurrentState(newEvent);
}
else
{
DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!");
}
}
public void ForceTraitorEvent(TraitorEventPrefab traitorEventPrefab)
{
if (eventManager == null)
{
throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly.");
}
var traitor = SelectRandomTraitor();
if (traitor == null)
{
return "";
DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\".");
return;
}
return traitor.Role;
}
public TraitorManager()
{
}
public void Start(GameServer server)
{
#if DISABLE_MISSIONS
return;
#endif
if (server == null) { return; }
ShouldEndRound = false;
this.server = server;
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)RandomDouble());
CreateTraitorEvent(eventManager, traitorEventPrefab, traitor);
}
public void SkipStartDelay()
{
startCountdown = 0.01f;
startTimer = 0.0f;
if (activeEvents.Any()) { started = true; }
}
public void Update(float deltaTime)
{
if (ShouldEndRound) { return; }
#if DISABLE_MISSIONS
return;
#endif
if (Missions.Any())
if (!Enabled) { return; }
if (!started)
{
bool missionCompleted = false;
bool gameShouldEnd = false;
CharacterTeamType winningTeam = CharacterTeamType.None;
foreach (var mission in Missions)
if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection })
{
mission.Value.Update(deltaTime, () =>
if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2)
{
switch (mission.Key)
{
case CharacterTeamType.Team1:
winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team2 : CharacterTeamType.None;
break;
case CharacterTeamType.Team2:
winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team1 : CharacterTeamType.None;
break;
default:
break;
}
gameShouldEnd = true;
});
if (!gameShouldEnd && mission.Value.IsCompleted)
{
missionCompleted = true;
foreach (var traitor in mission.Value.Traitors.Values)
{
traitor.UpdateCurrentObjective("", mission.Value.Identifier);
}
//try starting ASAP if the submarine is already half-way through the level
//(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below)
startTimer = Math.Min(startTimer, 10.0f);
}
}
if (gameShouldEnd)
startTimer -= deltaTime;
if (startTimer >= 0.0f) { return; }
if (eventManager == null)
{
GameMain.GameSession.WinningTeam = winningTeam;
ShouldEndRound = true;
return;
throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly.");
}
if (missionCompleted)
if (level == null)
{
Missions.Clear();
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
throw new InvalidOperationException("Level was null. TraitorManager may not have been initialized properly.");
}
if (TryCreateTraitorEvents(eventManager, level))
{
started = true;
}
else
{
//restart timer, we might be able to start a mission later if more clients join
startTimer = Rand.Range(StartDelayMin, StartDelayMax);
}
}
else if (startCountdown > 0.0f && server.GameStarted)
}
public void EndRound()
{
Client? votedAsTraitor = GetClientAccusedAsTraitor();
foreach (var activeEvent in activeEvents)
{
startCountdown -= deltaTime;
if (startCountdown <= 0.0f)
if (results != null)
{
int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0);
if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount)
DebugConsole.AddWarning("Multiple traitor events active during the round, only displaying the results for the last one.");
}
results = new TraitorResults(votedAsTraitor, activeEvent.TraitorEvent);
if (results.Value.MoneyPenalty > 0)
{
GameMain.GameSession?.Campaign?.Bank?.TryDeduct(results.Value.MoneyPenalty);
}
if (activeEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed)
{
activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed;
}
GameServer.Log(
NetworkMember.ClientLogName(activeEvent.Traitor) +
(activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed ?
" completed their traitor objective successfully." : " failed to complete their traitor objective."),
ServerLog.MessageType.Traitors);
//consider the event failed if the traitor was identifier correctly,
//so the traitor doesn't get rewards or get assigned a follow-up event
if (results.Value.VotedCorrectTraitor)
{
GameServer.Log(
NetworkMember.ClientLogName(activeEvent.Traitor) + " was correctly identified as the traitor, and will not receive any rewards.",
ServerLog.MessageType.Traitors);
activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed;
}
previousTraitorEvents.Add(new PreviousTraitorEvent(
activeEvent.TraitorEvent.Prefab,
activeEvent.TraitorEvent.CurrentState,
activeEvent.Traitor));
}
if (previousTraitorEvents.Count > MaxPreviousEventHistory)
{
previousTraitorEvents.RemoveRange(0, previousTraitorEvents.Count - MaxPreviousEventHistory);
}
activeEvents.Clear();
}
public Client? GetClientAccusedAsTraitor()
{
Client? votedAsTraitor = Voting.HighestVoted<Client>(VoteType.Traitor, server.ConnectedClients.Where(c => c.Character is { IsDead: false }), out int voteCount);
if (voteCount < server.ConnectedClients.Count * server.ServerSettings.MinPercentageOfPlayersForTraitorAccusation / 100.0f)
{
//at least x% of the players must've voted for the same player
votedAsTraitor = null;
}
return votedAsTraitor;
}
public TraitorResults? GetEndResults()
{
return results;
}
public XElement Save()
{
var element = new XElement(nameof(TraitorManager),
new XAttribute("version", GameMain.Version.ToString()));
foreach (var previousEvent in previousTraitorEvents)
{
previousEvent.Save(element);
}
return element;
}
public void Load(XElement traitorManagerElement)
{
previousTraitorEvents.Clear();
foreach (XElement subElement in traitorManagerElement.Elements())
{
if (subElement.Name.ToIdentifier() == nameof(PreviousTraitorEvent))
{
var previousTraitorEvent = PreviousTraitorEvent.Load(subElement);
if (previousTraitorEvent != null)
{
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
return;
previousTraitorEvents.Add(previousTraitorEvent);
}
if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) <= 1)
{
return;
}
if (GameMain.GameSession.Missions.Any(m => m is CombatMission))
{
var teamIds = new[] { CharacterTeamType.Team1, CharacterTeamType.Team2 };
foreach (var teamId in teamIds)
{
if (server.ConnectedClients.Count(c => c.Character != null && !c.Character.IsDead && c.TeamID == teamId) < 2)
{
continue;
}
var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate();
if (mission != null)
{
Missions.Add(teamId, mission);
}
}
var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key) ? 1 : 0);
if (canBeStartedCount >= Missions.Count)
{
var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key) ? 1 : 0);
if (startSuccessCount >= Missions.Count)
{
return;
}
}
}
else
{
var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate();
if (mission != null)
{
if (mission.CanBeStarted(server, this, CharacterTeamType.None))
{
if (mission.Start(server, this, CharacterTeamType.None))
{
Missions.Add(CharacterTeamType.None, mission);
return;
}
}
}
}
Missions.Clear();
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
}
}
}
public List<TraitorMissionResult> GetEndResults()
public void SendCurrentState(TraitorEvent ev)
{
List<TraitorMissionResult> results = new List<TraitorMissionResult>();
#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;
if (ev?.Traitor == null) { return; }
var msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE);
msg.WriteByte((byte)ev.CurrentState);
msg.WriteIdentifier(ev.Prefab.Identifier);
server.SendTraitorMessage(msg, ev.Traitor);
}
}
}
}
@@ -1,394 +0,0 @@
//#define ALLOW_SOLO_TRAITOR
//#define ALLOW_NONHUMANOID_TRAITOR
using System;
using Barotrauma.Networking;
using Lidgren.Network;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Security.Cryptography;
using Barotrauma.Extensions;
namespace Barotrauma
{
partial class Traitor
{
public class TraitorMission
{
private static string wordsTxt = Path.Combine("Content", "CodeWords.txt");
private readonly List<Objective> allObjectives = new List<Objective>();
private readonly List<Objective> pendingObjectives = new List<Objective>();
private readonly List<Objective> completedObjectives = new List<Objective>();
/// <summary>
/// Has the mission been completed (does not mean that the traitor necessarily won, the mission is considered completed if the traitor fails for whatever reason)
/// </summary>
public bool IsCompleted => pendingObjectives.Count <= 0;
public readonly Dictionary<string, Traitor> Traitors = new Dictionary<string, Traitor>();
public delegate bool RoleFilter(Character character);
public readonly Dictionary<string, RoleFilter> Roles = new Dictionary<string, RoleFilter>();
public string StartText { get; private set; }
public string CodeWords { get; private set; }
public string CodeResponse { get; private set; }
public string GlobalEndMessageSuccessTextId { get; private set; }
public string GlobalEndMessageSuccessDeadTextId { get; private set; }
public string GlobalEndMessageSuccessDetainedTextId { get; private set; }
public string GlobalEndMessageFailureTextId { get; private set; }
public string GlobalEndMessageFailureDeadTextId { get; private set; }
public string GlobalEndMessageFailureDetainedTextId { get; private set; }
public readonly Identifier Identifier;
public virtual IEnumerable<string> GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" };
public virtual IEnumerable<string> GlobalEndMessageValues {
get {
var isSuccess = completedObjectives.Count >= allObjectives.Count;
return new string[] {
string.Join(", ", Traitors.Values.Select(traitor => traitor.Character?.Name ?? "(unknown)")),
(isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? ""
};
}
}
public string GlobalEndMessage
{
get
{
if (Traitors.Any() && allObjectives.Count > 0)
{
return TextManager.JoinServerMessages("\n",
Traitors.Values.Select(traitor =>
{
var isSuccess = completedObjectives.Count >= allObjectives.Count;
var traitorIsDead = traitor.Character.IsDead;
var traitorIsDetained = traitor.Character.LockHands;
var messageId = isSuccess
? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId)
: (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId);
return TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, messageId, GlobalEndMessageKeys.Zip(GlobalEndMessageValues).ToArray());
}).ToArray());
}
return "";
}
}
public Objective GetCurrentObjective(Traitor traitor)
{
if (!Traitors.ContainsValue(traitor) || pendingObjectives.Count <= 0)
{
return null;
}
return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role));
}
protected List<Tuple<Client, Character>> FindTraitorCandidates(GameServer server, CharacterTeamType team, RoleFilter traitorRoleFilter)
{
var traitorCandidates = new List<Tuple<Client, Character>>();
foreach (Client c in server.ConnectedClients)
{
if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) ||
(team != CharacterTeamType.None && c.Character.TeamID != team))
{
continue;
}
#if !ALLOW_NONHUMANOID_TRAITOR
if (!c.Character.IsHumanoid) { continue; }
#endif
traitorCandidates.Add(Tuple.Create(c, c.Character));
}
return traitorCandidates;
}
protected List<Character> FindCharacters()
{
List<Character> characters = new List<Character>();
foreach (var character in Character.CharacterList)
{
characters.Add(character);
}
return characters;
}
protected List<Tuple<string, Tuple<Client, Character>>> AssignTraitors(GameServer server, TraitorManager traitorManager, CharacterTeamType team)
{
List<Character> characters = FindCharacters();
#if !ALLOW_SOLO_TRAITOR
if (characters.Count < 2)
{
return null;
}
#endif
var roleCandidates = new Dictionary<string, HashSet<Tuple<Client, Character>>>();
foreach (var role in Roles)
{
roleCandidates.Add(role.Key, new HashSet<Tuple<Client, Character>>(FindTraitorCandidates(server, team, role.Value)));
if (roleCandidates[role.Key].Count <= 0)
{
return null;
}
}
var candidateRoleCounts = new Dictionary<Tuple<Client, Character>, int>();
foreach (var candidateEntry in roleCandidates)
{
foreach (var candidate in candidateEntry.Value)
{
candidateRoleCounts[candidate] = candidateRoleCounts.TryGetValue(candidate, out var count) ? count + 1 : 1;
}
}
var unassignedRoles = new List<string>(roleCandidates.Keys);
unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count);
var assignedCandidates = new List<Tuple<string, Tuple<Client, Character>>>();
while (unassignedRoles.Count > 0)
{
var currentRole = unassignedRoles[0];
var availableCandidates = roleCandidates[currentRole].ToList();
if (availableCandidates.Count <= 0)
{
break;
}
unassignedRoles.RemoveAt(0);
availableCandidates.Sort((a, b) => candidateRoleCounts[b] - candidateRoleCounts[a]);
unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count);
int numCandidates = 1;
for (int i = 1; i < availableCandidates.Count && candidateRoleCounts[availableCandidates[i]] == candidateRoleCounts[availableCandidates[0]]; ++i)
{
++numCandidates;
}
var selected = ToolBox.SelectWeightedRandom(availableCandidates, availableCandidates.Select(c => Math.Max(c.Item1.RoundsSincePlayedAsTraitor, 0.1f)).ToList(), TraitorManager.Random);
assignedCandidates.Add(Tuple.Create(currentRole, selected));
foreach (var candidate in roleCandidates.Values)
{
candidate.Remove(selected);
}
}
if (unassignedRoles.Count > 0)
{
return null;
}
return assignedCandidates;
}
public bool CanBeStarted(GameServer server, TraitorManager traitorManager, CharacterTeamType team)
{
foreach (var role in Roles)
{
var candidates = FindTraitorCandidates(server, team, role.Value);
if (candidates.Count <= 0)
{
return false;
}
}
return AssignTraitors(server, traitorManager, team) != null;
}
public bool Start(GameServer server, TraitorManager traitorManager, CharacterTeamType team)
{
var assignedCandidates = AssignTraitors(server, traitorManager, team);
if (assignedCandidates == null)
{
return false;
}
foreach (Client client in server.ConnectedClients)
{
client.RoundsSincePlayedAsTraitor++;
}
Traitors.Clear();
foreach (var candidate in assignedCandidates)
{
var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character);
Traitors.Add(candidate.Item1, traitor);
candidate.Item2.Item1.RoundsSincePlayedAsTraitor = 0;
}
CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
if (pendingObjectives.Count <= 0 || !pendingObjectives[0].CanBeStarted(Traitors.Values))
{
Traitors.Clear();
return false;
}
var pendingMessages = new Dictionary<Traitor, List<string>>();
pendingMessages.Clear();
foreach (var traitor in Traitors.Values)
{
pendingMessages.Add(traitor, new List<string>());
}
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));
Update(0.0f, () => { GameMain.Server.TraitorManager.ShouldEndRound = true; });
#if SERVER
foreach (var traitor in Traitors.Values)
{
GameServer.Log($"{GameServer.CharacterLogName(traitor.Character)} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage);
}
#endif
return true;
}
public delegate void TraitorWinHandler();
public void Update(float deltaTime, TraitorWinHandler winHandler)
{
if (pendingObjectives.Count <= 0 || Traitors.Count <= 0)
{
return;
}
if (Traitors.Values.Any(traitor => traitor.Character == null || traitor.Character.IsDead || traitor.Character.Removed))
{
Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier));
pendingObjectives.Clear();
Traitors.Clear();
return;
}
var startedObjectives = new List<Objective>();
foreach (var traitor in Traitors.Values)
{
startedObjectives.Clear();
while (pendingObjectives.Count > 0)
{
var objective = GetCurrentObjective(traitor);
if (objective == null)
{
// No more objectives left for traitor or waiting for another traitor's objective.
break;
}
if (!objective.IsStarted)
{
if (!objective.Start(traitor))
{
//the mission fails if an objective cannot be started
if (completedObjectives.Count > 0)
{
objective.EndMessage();
}
pendingObjectives.Clear();
break;
}
startedObjectives.Add(objective);
}
objective.Update(deltaTime);
if (objective.IsCompleted)
{
pendingObjectives.Remove(objective);
completedObjectives.Add(objective);
objective.EndMessage();
continue;
}
if (objective.IsStarted && !objective.CanBeCompleted)
{
objective.EndMessage();
pendingObjectives.Clear();
}
break;
}
if (pendingObjectives.Count > 0)
{
startedObjectives.ForEach(objective => objective.StartMessage());
}
}
if (completedObjectives.Count >= allObjectives.Count)
{
foreach (var traitor in Traitors)
{
SteamAchievementManager.OnTraitorWin(traitor.Value.Character);
}
winHandler();
}
}
public delegate bool CharacterFilter(Character character);
public List<Character> FindKillTarget(Character traitor, CharacterFilter filter, int count = -1, float percentage = -1f)
{
if (traitor == null) { return null; }
List<Character> validCharacters = Character.CharacterList.FindAll(c => c.TeamID == traitor.TeamID &&
c != traitor && !c.IsDead &&
(filter == null || filter(c)));
int targetCount = 1;
if (count > 0)
{
targetCount = count;
}
else if (percentage > 0f)
{
targetCount = (int)Math.Max(1, Math.Floor(validCharacters.Count * percentage));
}
List<Character> targetCharacters = new List<Character>();
if (validCharacters.Count > 0)
{
for (int i = 0; i < targetCount; i++)
{
if (validCharacters.Count == 0) break;
Character character = validCharacters[TraitorManager.RandomInt(validCharacters.Count)];
targetCharacters.Add(character);
validCharacters.Remove(character);
}
return targetCharacters;
}
#if ALLOW_SOLO_TRAITOR
targetCharacters.Add(traitor);
return targetCharacters;
#else
return null;
#endif
}
public string GetTargetNames(List<Character> targets)
{
string names = string.Empty;
for (int i = 0; i < targets.Count; i++)
{
names += targets[i].Name;
if (i < targets.Count - 1)
{
names += ", ";
}
}
if (names.Length > 0)
{
return names;
}
else
{
return TextManager.FormatServerMessage("unknown");
}
}
public TraitorMission(Identifier identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable<KeyValuePair<string, RoleFilter>> roles, ICollection<Objective> objectives)
{
Identifier = identifier;
StartText = startText;
GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId;
GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId;
GlobalEndMessageSuccessDetainedTextId = globalEndMessageSuccessDetainedTextId;
GlobalEndMessageFailureTextId = globalEndMessageFailureTextId;
GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId;
GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId;
foreach (var role in roles)
{
Roles.Add(role.Key, role.Value);
}
allObjectives.AddRange(objectives);
pendingObjectives.AddRange(objectives);
}
}
}
}
@@ -1,664 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
class TraitorMissionPrefab
{
public class TraitorMissionEntry : Prefab
{
public static PrefabCollection<TraitorMissionEntry> Prefabs => TraitorMissionPrefab.Prefabs;
public readonly TraitorMissionPrefab Prefab;
public float SelectedWeight;
public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element)
{
Prefab = new TraitorMissionPrefab(element);
}
public override void Dispose() { }
}
public static readonly PrefabCollection<TraitorMissionEntry> Prefabs = new PrefabCollection<TraitorMissionEntry>();
public static TraitorMissionPrefab RandomPrefab()
{
var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random);
//the weight of the missions that didn't get selected keeps growing the make them more likely to get picked
foreach (var mission in Prefabs)
{
mission.SelectedWeight += 10;
}
selected.SelectedWeight = 0.0f;
return selected.Prefab;
}
private class AttributeChecker : IDisposable
{
private readonly XElement element;
private readonly HashSet<string> required = new HashSet<string>();
private readonly HashSet<string> optional = new HashSet<string>();
public void Optional(params string[] names)
{
optional.UnionWith(names);
}
public void Required(params string[] names)
{
required.UnionWith(names);
}
public void Dispose()
{
foreach (var requiredName in required)
{
if (element.Attributes().All(attribute => attribute.Name != requiredName))
{
GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error);
}
}
foreach (var attribute in element.Attributes())
{
var attributeName = attribute.Name.ToString();
if (!required.Contains(attributeName) && !optional.Contains(attributeName))
{
GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error);
}
}
}
public AttributeChecker(XElement element)
{
this.element = element;
}
}
public class Goal
{
public readonly string Type;
public readonly XElement Config;
public Goal(string type, XElement config)
{
Type = type;
Config = config;
}
private delegate bool TargetFilter(string value, Character character);
private static Dictionary<string, TargetFilter> targetFilters = new Dictionary<string, TargetFilter>()
{
{ "job", (value, character) => value == character.Info.Job.Prefab.Identifier },
{ "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) }
};
public Traitor.Goal Instantiate()
{
Traitor.Goal goal = null;
using (var checker = new AttributeChecker(Config))
{
checker.Required("type");
var goalType = Config.GetAttributeString("type", "");
switch (goalType.ToLowerInvariant())
{
case "killtarget":
{
checker.Optional(targetFilters.Keys.ToArray());
checker.Optional("causeofdeath");
checker.Optional("affliction");
checker.Optional("roomname");
checker.Optional("targetcount");
checker.Optional("targetpercentage");
List<Traitor.TraitorMission.CharacterFilter> killFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
killFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)),
(CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true),
Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1),
Config.GetAttributeFloat("targetpercentage", -1f));
break;
}
case "destroyitems":
{
checker.Required("tag");
checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory");
var tag = Config.GetAttributeString("tag", null);
if (tag != null)
{
goal = new Traitor.GoalDestroyItemsWithTag(
tag,
Config.GetAttributeFloat("percentage", 100.0f) / 100.0f,
Config.GetAttributeBool("matchIdentifier", true),
Config.GetAttributeBool("matchTag", true),
Config.GetAttributeBool("matchInventory", false));
}
break;
}
case "sabotage":
{
checker.Required("tag");
checker.Optional("threshold");
var tag = Config.GetAttributeString("tag", null);
if (tag != null)
{
goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f));
}
break;
}
case "floodsub":
checker.Optional("percentage");
goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
break;
case "finditem":
checker.Required("identifier");
checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage");
List<Traitor.TraitorMission.CharacterFilter> itemCountFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
itemCountFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)),
Config.GetAttributeString("identifier",
null),
Config.GetAttributeBool("preferNew",
true),
Config.GetAttributeBool("allowNew",
true),
Config.GetAttributeBool("allowExisting",
true),
Config.GetAttributeFloat("percentage",
-1f),
Config.GetAttributeIdentifierArray("allowedContainers",
new string[]
{
"steelcabinet",
"mediumsteelcabinet",
"suppliescabinet"
}.ToIdentifiers()));
break;
case "replaceinventory":
checker.Required("containers", "replacements");
checker.Optional("percentage");
goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
break;
case "reachdistancefromsub":
checker.Optional("distance");
goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f));
break;
case "injectpoison":
checker.Optional(targetFilters.Keys.ToArray());
checker.Required("poison");
checker.Required("affliction");
checker.Optional("targetcount");
checker.Optional("targetpercentage");
List<Traitor.TraitorMission.CharacterFilter> poisonFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
poisonFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null),
Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f));
break;
case "unwire":
checker.Required("tag");
checker.Optional("connectionname");
checker.Optional("connectiondisplayname");
goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null));
break;
case "transformentity":
checker.Required("entities", "entitytypes");
checker.Optional("catalystid");
goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null));
break;
case "keeptransformedalive":
checker.Required("speciesname");
goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty));
break;
default:
GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error);
break;
}
}
if (goal == null)
{
return null;
}
foreach (var element in Config.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "modifier":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("type");
var modifierType = element.GetAttributeString("type", "");
switch (modifierType)
{
case "duration":
{
checker.Optional("cumulative", "duration", "infotext");
var isCumulative = element.GetAttributeBool("cumulative", false);
goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText"));
break;
}
case "timelimit":
checker.Optional("timelimit", "infotext");
goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText"));
break;
case "optional":
checker.Optional("infotext");
goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText"));
break;
default:
GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error);
break;
}
}
break;
}
}
}
foreach (var element in Config.Elements())
{
var elementName = element.Name.ToString().ToLowerInvariant();
switch (elementName)
{
case "modifier":
// loaded above
break;
case "infotext":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("id");
var id = element.GetAttributeString("id", null);
if (id != null)
{
goal.InfoTextId = id;
}
}
break;
}
case "completedtext":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("id");
var id = element.GetAttributeString("id", null);
if (id != null)
{
goal.CompletedTextId = id;
}
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error);
break;
}
}
return goal;
}
}
public abstract class ObjectiveBase
{
public HashSet<string> Roles { get; } = new HashSet<string>();
public abstract void InstantiateGoals();
public abstract Traitor.Objective Instantiate(IEnumerable<string> roles);
}
protected class Objective : ObjectiveBase
{
public string InfoText { get; internal set; }
public string StartMessageTextId { get; internal set; }
public string StartMessageServerTextId { get; internal set; }
public string EndMessageSuccessTextId { get; internal set; }
public string EndMessageSuccessDeadTextId { get; internal set; }
public string EndMessageSuccessDetainedTextId { get; internal set; }
public string EndMessageFailureTextId { get; internal set; }
public string EndMessageFailureDeadTextId { get; internal set; }
public string EndMessageFailureDetainedTextId { get; internal set; }
public int ShuffleGoalsCount { get; internal set; }
public readonly List<Goal> Goals = new List<Goal>();
private List<Traitor.Goal> goalInstances = null;
public override void InstantiateGoals()
{
goalInstances = Goals.ConvertAll(goal =>
{
var instance = goal.Instantiate();
if (instance == null)
{
GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error);
}
return instance;
}).FindAll(goal => goal != null);
}
public override Traitor.Objective Instantiate(IEnumerable<string> roles)
{
var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances);
if (StartMessageTextId != null)
{
result.StartMessageTextId = StartMessageTextId;
}
if (StartMessageServerTextId != null)
{
result.StartMessageServerTextId = StartMessageServerTextId;
}
if (EndMessageSuccessTextId != null)
{
result.EndMessageSuccessTextId = EndMessageSuccessTextId;
}
if (EndMessageSuccessDeadTextId != null)
{
result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId;
}
if (EndMessageSuccessDetainedTextId != null)
{
result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId;
}
if (EndMessageFailureTextId != null)
{
result.EndMessageFailureTextId = EndMessageFailureTextId;
}
if (EndMessageFailureDeadTextId != null)
{
result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId;
}
if (EndMessageFailureDetainedTextId != null)
{
result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId;
}
return result;
}
}
protected class WaitObjective : ObjectiveBase
{
private Traitor.GoalWaitForTraitors sharedGoal;
public override void InstantiateGoals()
{
sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count);
}
public override Traitor.Objective Instantiate(IEnumerable<string> roles)
{
return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal });
}
public WaitObjective(ICollection<string> roles)
{
Roles.UnionWith(roles);
}
}
public class Role
{
public readonly Traitor.TraitorMission.RoleFilter Filter;
public Role(IEnumerable<Traitor.TraitorMission.RoleFilter> filters)
{
Filter = character => filters.All(filter => filter(character));
}
public Role()
{
Filter = character => true;
}
}
public readonly Dictionary<string, Role> Roles = new Dictionary<string, Role>();
public readonly Identifier Identifier;
public readonly string StartText;
public readonly string EndMessageSuccessText;
public readonly string EndMessageSuccessDeadText;
public readonly string EndMessageSuccessDetainedText;
public readonly string EndMessageFailureText;
public readonly string EndMessageFailureDeadText;
public readonly string EndMessageFailureDetainedText;
public readonly List<ObjectiveBase> Objectives = new List<ObjectiveBase>();
public Traitor.TraitorMission Instantiate()
{
var objectivesWithSync = new List<ObjectiveBase>();
var objectivesCount = Objectives.Count;
if (objectivesCount > 0)
{
var pendingRoles = new HashSet<string>();
var pendingCount = 1;
objectivesWithSync.Add(Objectives[0]);
pendingRoles.UnionWith(Objectives[0].Roles);
for (var i = 1; i < objectivesCount; ++i)
{
var objective = Objectives[i];
if (pendingRoles.IsSupersetOf(objective.Roles))
{
if (pendingCount > 1)
{
objectivesWithSync.Add(new WaitObjective(objective.Roles));
}
pendingRoles.Clear();
pendingCount = 0;
}
objectivesWithSync.Add(objective);
pendingRoles.UnionWith(objective.Roles);
++pendingCount;
}
if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys))
{
// TODO: If last objective includes only one traitor, other traitors will get the wrong end message.
objectivesWithSync.Add(new WaitObjective(Roles.Keys));
}
}
return new Traitor.TraitorMission(
Identifier,
StartText ?? "TraitorMissionStartMessage",
EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess",
EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead",
EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained",
EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure",
EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead",
EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained",
Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter),
objectivesWithSync.SelectMany(objective =>
{
objective.InstantiateGoals();
return objective.Roles.Select(role => objective.Instantiate(new[] { role }));
}).ToArray());
}
protected Goal LoadGoal(XElement goalRoot)
{
var goalType = goalRoot.GetAttributeString("type", "");
return new Goal(goalType, goalRoot);
}
protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles)
{
var allRolesSet = new HashSet<string>(allRoles);
var result = new Objective
{
ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1)
};
var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles);
if (!allRolesSet.IsSupersetOf(objectiveRoles))
{
var unrecognized = new HashSet<string>(objectiveRoles);
unrecognized.ExceptWith(allRoles);
GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error);
}
result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles));
foreach (var element in objectiveRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "infotext":
checker.Required("id");
result.InfoText = element.GetAttributeString("id", null);
break;
case "startmessage":
checker.Required("id");
result.StartMessageTextId = element.GetAttributeString("id", null);
break;
case "startmessageserver":
checker.Required("id");
result.StartMessageServerTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccess":
checker.Required("id");
result.EndMessageSuccessTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdead":
checker.Required("id");
result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdetained":
checker.Required("id");
result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailure":
checker.Required("id");
result.EndMessageFailureTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailuredead":
checker.Required("id");
result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailuredetained":
checker.Required("id");
result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null);
break;
case "goal":
{
var goal = LoadGoal(element);
if (goal != null)
{
result.Goals.Add(goal);
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error);
break;
}
}
}
return result;
}
protected Role LoadRole(XElement roleRoot)
{
var filters = new List<Traitor.TraitorMission.RoleFilter>();
var jobs = roleRoot.GetAttributeStringArray("jobs", null);
if (jobs != null)
{
var jobsSet = new HashSet<string>(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture)));
filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value));
}
return new Role(filters);
}
public TraitorMissionPrefab(ContentXElement missionRoot)
{
Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty);
foreach (var element in missionRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "role":
checker.Required("id");
checker.Optional("jobs");
Roles.Add(element.GetAttributeString("id", null), LoadRole(element));
break;
}
}
}
if (!Roles.Any())
{
Roles.Add("traitor", new Role());
}
foreach (var element in missionRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "role":
// handled above
break;
case "startinfotext":
checker.Required("id");
StartText = element.GetAttributeString("id", null);
break;
case "endmessagesuccess":
checker.Required("id");
EndMessageSuccessText = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdead":
checker.Required("id");
EndMessageSuccessDeadText = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdetained":
checker.Required("id");
EndMessageSuccessDetainedText = element.GetAttributeString("id", null);
break;
case "endmessagefailure":
checker.Required("id");
EndMessageFailureText = element.GetAttributeString("id", null);
break;
case "endmessagefailuredead":
checker.Required("id");
EndMessageFailureDeadText = element.GetAttributeString("id", null);
break;
case "endmessagefailuredetained":
checker.Required("id");
EndMessageFailureDetainedText = element.GetAttributeString("id", null);
break;
case "objective":
{
var objective = LoadObjective(element, Roles.Keys.ToArray());
if (objective != null)
{
Objectives.Add(objective);
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error);
break;
}
}
}
}
}
}
@@ -1,30 +0,0 @@
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.WriteIdentifier(MissionIdentifier);
msg.WriteString(EndMessage);
msg.WriteBoolean(Success);
msg.WriteByte((byte)Characters.Count);
foreach (Character character in Characters)
{
msg.WriteUInt16(character.ID);
}
}
}
}