Unstable 1.1.14.0
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
+6
-3
@@ -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");
|
||||
|
||||
+7
-3
@@ -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);
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user