Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs
2026-04-09 15:10:07 +03:00

635 lines
28 KiB
C#

using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
partial class KarmaManager : ISerializableEntity
{
private class ClientMemory
{
public List<Pair<Wire, float>> WireDisconnectTime = new List<Pair<Wire, float>>();
public struct TimeAmount
{
public double Time;
public float Amount;
}
public List<TimeAmount> KarmaDecreasesInPastMinute = new List<TimeAmount>();
public float PreviousNotifiedKarma;
public double PreviousKarmaNotificationTime;
public float StructureDamageAccumulator;
private float structureDamagePerSecond;
public float StructureDamagePerSecond
{
get { return Math.Max(StructureDamageAccumulator, structureDamagePerSecond); }
set { structureDamagePerSecond = value; }
}
public List<TimeAmount> StunsInPastMinute = new List<TimeAmount>();
public float StunKarmaDecreaseMultiplier;
//when did a given character last attack this one
public Dictionary<Character, double> LastAttackTime
{
get;
private set;
} = new Dictionary<Character, double>();
public int DangerousItemsContained { get; set; }
}
public bool TestMode = false;
private readonly Dictionary<Client, ClientMemory> clientMemories = new Dictionary<Client, ClientMemory>();
private readonly List<Client> bannedClients = new List<Client>();
private DateTime perSecondUpdate;
public void UpdateClients(IEnumerable<Client> clients, float deltaTime)
{
if (!GameMain.Server.GameStarted) { return; }
bannedClients.Clear();
foreach (Client client in clients)
{
UpdateClient(client, deltaTime);
if (perSecondUpdate < DateTime.Now)
{
var clientMemory = GetClientMemory(client);
clientMemory.StructureDamagePerSecond = clientMemory.StructureDamageAccumulator;
clientMemory.StructureDamageAccumulator = 0.0f;
clientMemory.StunsInPastMinute.RemoveAll(s => s.Time + 60.0f < Timing.TotalTime);
if (!clientMemory.StunsInPastMinute.Any())
{
clientMemory.StunKarmaDecreaseMultiplier = 1.0f;
}
var toRemove = clientMemory.LastAttackTime.Where(pair => pair.Value < Timing.TotalTime - AllowedRetaliationTime).Select(pair => pair.Key).ToList();
foreach (var lastAttacker in toRemove)
{
clientMemory.LastAttackTime.Remove(lastAttacker);
}
}
}
if (perSecondUpdate < DateTime.Now)
{
foreach (Client client in clients)
{
SendKarmaNotifications(client);
}
perSecondUpdate = DateTime.Now + new TimeSpan(0, 0, 1);
}
foreach (Client bannedClient in bannedClients)
{
bannedClient.KarmaKickCount++;
if (bannedClient.KarmaKickCount <= KicksBeforeBan)
{
GameMain.Server.KickClient(bannedClient, $"KarmaKicked~[banthreshold]={(int)KickBanThreshold}", resetKarma: true);
}
else
{
GameMain.Server.BanClient(bannedClient, $"KarmaBanned~[banthreshold]={(int)KickBanThreshold}", duration: TimeSpan.FromSeconds(GameMain.Server.ServerSettings.AutoBanTime));
}
}
}
private void SendKarmaNotifications(Client client, string debugKarmaChangeReason = "")
{
//send a notification about karma changing if the karma has changed by x%
var clientMemory = GetClientMemory(client);
float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma;
if (Math.Abs(karmaChange) > 1.0f && TestMode)
{
string msg =
karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}";
if (!string.IsNullOrEmpty(debugKarmaChangeReason))
{
msg += $". Reason: {debugKarmaChangeReason}";
}
GameMain.Server.SendDirectChatMessage(msg, client);
clientMemory.PreviousNotifiedKarma = client.Karma;
clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime;
}
else if (Timing.TotalTime >= clientMemory.PreviousKarmaNotificationTime + 5.0f &&
clientMemory.PreviousNotifiedKarma >= KickBanThreshold + KarmaNotificationInterval &&
client.Karma < KickBanThreshold + KarmaNotificationInterval)
{
GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning").Value, client);
GameServer.Log(GameServer.ClientLogName(client) + " has been warned for having dangerously low karma.", ServerLog.MessageType.Karma);
clientMemory.PreviousNotifiedKarma = client.Karma;
clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime;
}
}
private void UpdateClient(Client client, float deltaTime)
{
if (client.Character != null && !client.Character.Removed && !client.Character.IsDead)
{
if (client.Karma > KarmaDecayThreshold)
{
client.Karma -= KarmaDecay * deltaTime;
}
else if (client.Karma < KarmaIncreaseThreshold)
{
client.Karma += KarmaIncrease * deltaTime;
}
//increase the strength of the herpes affliction in steps instead of linearly
//otherwise clients could determine their exact karma value from the strength
float herpesStrength = 0.0f;
if (client.Karma < HerpesThreshold * 0.5f)
herpesStrength = 100.0f;
else if (client.Karma < HerpesThreshold * 0.75f)
herpesStrength = 60.0f;
else if (client.Karma < HerpesThreshold)
herpesStrength = 30.0f;
var existingAffliction = client.Character.CharacterHealth.GetAffliction<AfflictionSpaceHerpes>(AfflictionPrefab.SpaceHerpesType);
if (existingAffliction == null && herpesStrength > 0.0f)
{
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma);
GameMain.NetworkMember.LastClientListUpdateID++;
}
else if (existingAffliction != null)
{
existingAffliction.Strength = herpesStrength;
if (herpesStrength <= 0.0f)
{
client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f);
}
}
//check if the client has disconnected an excessive number of wires
var clientMemory = GetClientMemory(client);
if (clientMemory.WireDisconnectTime.Count > (int)AllowedWireDisconnectionsPerMinute)
{
clientMemory.WireDisconnectTime.RemoveRange(0, clientMemory.WireDisconnectTime.Count - (int)AllowedWireDisconnectionsPerMinute);
if (clientMemory.WireDisconnectTime.All(w => Timing.TotalTime - w.Second < 60.0f))
{
float karmaDecrease = -WireDisconnectionKarmaDecrease;
//engineers don't lose as much karma for removing lots of wires
if (client.Character.Info?.Job.Prefab.Identifier == "engineer") { karmaDecrease *= 0.5f; }
AdjustKarma(client.Character, karmaDecrease, "Disconnected excessive number of wires");
}
}
if (client.Character?.Info?.Job.Prefab.Identifier == "captain" && client.Character.SelectedItem != null)
{
if (client.Character.SelectedItem.GetComponent<Steering>() != null)
{
AdjustKarma(client.Character, SteerSubKarmaIncrease * deltaTime, "Steering the sub");
}
}
}
if (client.Karma < KickBanThreshold && client.Connection != GameMain.Server.OwnerConnection)
{
if (TestMode)
{
client.Karma = 50.0f;
GameMain.Server.SendDirectChatMessage("BANNED! (not really because karma test mode is enabled)", client);
}
else
{
bannedClients.Add(client);
}
}
}
public void OnRoundEnded()
{
if (ResetKarmaBetweenRounds)
{
clientMemories.Clear();
foreach (Client client in GameMain.Server.ConnectedClients)
{
client.Karma = Math.Max(50.0f, client.Karma);
}
}
}
public void OnClientDisconnected(Client client)
{
clientMemories.Remove(client);
}
public void OnBallastFloraDamaged(Character character, float damage)
{
if (character == null) { return; }
float karmaChange = damage * BallastFloraKarmaIncrease;
AdjustKarma(character, karmaChange, "Damaged ballast flora");
}
// ReSharper disable once UseNegatedPatternMatching, LoopCanBeConvertedToQuery
public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, Item item)
{
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner);
Character thiefCharacter = yoinker?.Character;
Character targetCharacter = inventory.Owner as Character;
if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; }
if (thiefCharacter.TeamID != targetCharacter.TeamID) { 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 != null)
{
if (GameMain.Server.TraitorManager.IsTraitor(targetCharacter) ||
GameMain.Server.TraitorManager.IsTraitor(thiefCharacter))
{
// Don't penalize traitors
return;
}
}
Item foundItem = null;
if (IsValid(item))
{
foundItem = item;
}
else
{
foreach (Item containedItem in item.ContainedItems)
{
if (IsValid(containedItem))
{
foundItem = containedItem;
break;
}
}
}
static bool IsValid(Item item)
{
return item.GetComponent<IdCard>() != null || IsWeapon(item);
}
static bool IsWeapon(Item item)
{
//a threshold of 10 excludes things like tools, all "proper weapons" seem to have a priority higher than that
return item.Components.Max(c => c.CombatPriority) > 10.0f || item.HasTag(Tags.Weapon);
}
if (foundItem == null) { return; }
bool isIdCard = foundItem.GetComponent<IdCard>() != null;
if (isIdCard)
{
string name = string.Empty;
foreach (var tag in foundItem.Tags.Split(','))
{
string[] split = tag.Split(':');
string key = split.Length > 0 ? split[0] : string.Empty;
string value = split.Length > 1 ? split[1] : string.Empty;
if (key == "name") { name = value; }
}
// Name tag doesn't belong to anyone in particular or we own the ID card
if (name == null || name == thiefCharacter.Name) { return; }
}
if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; }
const float calcUpper = 1, calcLower = -1;
float upper = DangerousItemStealKarmaDecrease + 10.0f;
float lower = DangerousItemStealKarmaDecrease - 10.0f;
if (lower < 0)
{
upper += Math.Abs(lower);
lower = 0;
}
// If we're stealing from a bot assume the bot has 50 karma
var targetKarma = targetClient?.Karma ?? 50;
float karmaDifference = Math.Clamp((targetKarma - yoinker.Karma) / 50.0f, calcLower, calcUpper);
float karmaDecrease = lower + (karmaDifference - calcLower) * (upper - lower) / (calcUpper - calcLower);
JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab;
// security officers receive less karma penalty
if (clientJob != null && clientJob.Identifier == "securityofficer" && IsWeapon(foundItem))
{
karmaDecrease *= 0.5f;
}
AdjustKarma(thiefCharacter, -karmaDecrease, "Stolen dangerous item");
}
public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable<Affliction> appliedAfflictions = null)
{
if (target == null || attacker == null) { return; }
if (target == attacker) { return; }
//damaging dead characters doesn't affect karma
if (target.IsDead || target.Removed) { return; }
bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID;
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.IsTraitor(target))
{
//traitors always count as enemies
isEnemy = true;
}
if (GameMain.Server.TraitorManager.IsTraitor(attacker))
{
//others count as an enemies to the traitor
isEnemy = true;
}
}
static bool IsHusk(Character c) => c.IsHusk || c.IsHuskInfected;
//huskified characters count as enemies to healthy characters and vice versa
if (IsHusk(attacker) != IsHusk(target)) { isEnemy = true; }
if (appliedAfflictions != null)
{
foreach (Affliction affliction in appliedAfflictions)
{
if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; }
damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
}
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == target);
if (damage > 0 && targetClient != null)
{
var targetMemory = GetClientMemory(targetClient);
targetMemory.LastAttackTime[attacker] = Timing.TotalTime;
}
Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
ClientMemory attackerMemory = GetClientMemory(attackerClient);
if (attackerMemory != null)
{
//if the attacker has been attacked by the target within the last x seconds, ignore the damage
//(= no karma penalty from retaliating against someone who attacked you)
if (attackerMemory.LastAttackTime.ContainsKey(target) &&
attackerMemory.LastAttackTime[target] > Timing.TotalTime - AllowedRetaliationTime)
{
damage = Math.Min(damage, 0);
stun = 0.0f;
}
}
//attacking/healing clowns has a smaller effect on karma
if (target.HasEquippedItem("clownmask".ToIdentifier()) &&
target.HasEquippedItem("clowncostume".ToIdentifier()))
{
damage *= 0.5f;
stun *= 0.5f;
}
//smaller karma penalty for attacking someone who's aiming with a weapon
if (damage > 0.0f &&
target.IsKeyDown(InputType.Aim) &&
target.HeldItems.Any(it => it.GetComponent<MeleeWeapon>() != null || it.GetComponent<RangedWeapon>() != null))
{
damage *= 0.5f;
stun *= 0.5f;
}
//damage scales according to the karma of the target
//(= smaller karma penalty from attacking someone who has a low karma)
if (damage > 0 && targetClient != null)
{
damage *= MathUtils.InverseLerp(0.0f, 50.0f, targetClient.Karma);
}
if (isEnemy)
{
if (damage > 0)
{
float karmaIncrease = damage * DamageEnemyKarmaIncrease;
if (attacker?.Info?.Job.Prefab.Identifier == "securityofficer") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Damaged enemy");
}
}
else
{
if (stun > 0 && attackerMemory != null)
{
//GameServer.Log(GameServer.CharacterLogName(attacker) + " stunned " + GameServer.CharacterLogName(target) + $" ({stun})", ServerLog.MessageType.Karma);
attackerMemory.StunsInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = stun });
if (attackerMemory.StunsInPastMinute.Count > 1)
{
float avgStunsInflicted = attackerMemory.StunsInPastMinute[0].Amount / (float)(attackerMemory.StunsInPastMinute[1].Time - attackerMemory.StunsInPastMinute[0].Time);
for (int i = 1; i < attackerMemory.StunsInPastMinute.Count; i++)
{
avgStunsInflicted += attackerMemory.StunsInPastMinute[i].Amount / (float)(attackerMemory.StunsInPastMinute[i].Time - attackerMemory.StunsInPastMinute[i - 1].Time);
}
//GameServer.Log(avgStunsInflicted.ToString(), ServerLog.MessageType.Karma);
if (avgStunsInflicted > StunFriendlyKarmaDecreaseThreshold ||
attackerMemory.StunKarmaDecreaseMultiplier > 1.0f)
{
AdjustKarma(attacker, -StunFriendlyKarmaDecrease * attackerMemory.StunKarmaDecreaseMultiplier, "Stunned friendly");
attackerMemory.StunKarmaDecreaseMultiplier *= 2.0f;
}
}
}
if (damage > 0)
{
AdjustKarma(attacker, -damage * DamageFriendlyKarmaDecrease, "Damaged friendly");
}
else
{
float karmaIncrease = -damage * HealFriendlyKarmaIncrease;
if (attacker?.Info?.Job.Prefab.Identifier == "medicaldoctor") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Healed friendly");
}
}
}
public void OnStructureHealthChanged(Structure structure, Character attacker, float damageAmount)
{
if (attacker == null) { return; }
//damaging/repairing ruin structures or enemy subs doesn't affect karma
if (structure.Submarine == null || structure.Submarine.TeamID != attacker.TeamID)
{
return;
}
if (damageAmount > 0)
{
if (StructureDamageKarmaDecrease <= 0.0f) { return; }
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.IsTraitor(attacker))
{
//traitors are allowed to damage structures
return;
}
}
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
if (client != null)
{
//cap the damage so the karma can't decrease by more than MaxStructureDamageKarmaDecreasePerSecond per second
var clientMemory = GetClientMemory(client);
if (clientMemory.StructureDamagePerSecond + damageAmount >= MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease)
{
damageAmount -= (clientMemory.StructureDamagePerSecond + damageAmount) - (MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease);
if (damageAmount <= 0.0f) { return; }
}
clientMemory.StructureDamageAccumulator += damageAmount;
}
AdjustKarma(attacker, -damageAmount * StructureDamageKarmaDecrease, "Damaged structures");
}
else
{
float karmaIncrease = -damageAmount * StructureRepairKarmaIncrease;
//mechanics get twice as much karma for repairing walls
if (attacker.Info?.Job.Prefab.Identifier == "mechanic") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Repaired structures");
}
}
public void OnItemRepaired(Character character, Repairable repairable, float repairAmount)
{
float karmaIncrease = repairAmount * ItemRepairKarmaIncrease;
if (repairable.HasRequiredSkills(character)) { karmaIncrease *= 2.0f; }
AdjustKarma(character, karmaIncrease, "Repaired item");
}
public void OnReactorOverHeating(Item reactor, Character character, float deltaTime)
{
if (reactor?.Submarine == null || character == null) { return; }
if (reactor.Submarine.TeamID == CharacterTeamType.FriendlyNPC || reactor.Submarine.TeamID == character.TeamID)
{
AdjustKarma(character, -ReactorOverheatKarmaDecrease * deltaTime, "Caused reactor to overheat");
}
}
public void OnReactorMeltdown(Item reactor, Character character)
{
if (reactor?.Submarine == null || character == null) { return; }
if (reactor.Submarine.TeamID == CharacterTeamType.FriendlyNPC || reactor.Submarine.TeamID == character.TeamID)
{
AdjustKarma(character, -ReactorMeltdownKarmaDecrease, "Caused a reactor meltdown");
}
}
public void OnExtinguishingFire(Character character, float deltaTime)
{
AdjustKarma(character, ExtinguishFireKarmaIncrease * deltaTime, "Extinguished a fire");
}
public void OnWireDisconnected(Character character, Wire wire)
{
if (character == null || wire == null) { return; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (client == null) { return; }
if (!clientMemories.ContainsKey(client)) { clientMemories[client] = new ClientMemory(); }
clientMemories[client].WireDisconnectTime.RemoveAll(w => w.First == wire);
clientMemories[client].WireDisconnectTime.Add(new Pair<Wire, float>(wire, (float)Timing.TotalTime));
}
private ClientMemory GetClientMemory(Client client)
{
if (client == null) { return null; }
if (!clientMemories.ContainsKey(client))
{
clientMemories[client] = new ClientMemory()
{
PreviousNotifiedKarma = client.Karma
};
}
return clientMemories[client];
}
public void OnSpamFilterTriggered(Client client)
{
if (client != null)
{
client.Karma -= SpamFilterKarmaDecrease;
SendKarmaNotifications(client, "Triggered the spam filter");
}
}
public void OnItemContained(Item containedItem, Item container, Character character)
{
if (containedItem == null || container == null || character == null || character.IsTraitor) { return; }
if (container.Prefab.Identifier == Tags.WeldingFuel && containedItem.HasTag(Tags.OxygenSource))
{
var client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (client == null) { return; }
float amount = -DangerousItemContainKarmaDecrease;
var memory = GetClientMemory(client);
if (IsDangerousItemContainKarmaDecreaseIncremental)
{
amount *= memory.DangerousItemsContained;
}
amount = Math.Max(amount, -MaxDangerousItemContainKarmaDecrease);
AdjustKarma(character, amount, "Put an oxygen tank inside a welding tool");
clientMemories[client].DangerousItemsContained = memory.DangerousItemsContained + 1;
}
}
private void AdjustKarma(Character target, float amount, string debugKarmaChangeReason = "")
{
if (target == null) { return; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == target);
if (client == null) { return; }
//all penalties/rewards are halved when wearing a clown costume
if (target.HasEquippedItem("clownmask".ToIdentifier()) &&
target.HasEquippedItem("clowncostume".ToIdentifier()))
{
amount *= 0.5f;
}
client.Karma += amount;
if (amount < 0.0f)
{
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);
clientMemory.KarmaDecreasesInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = -amount });
if (herpesStrength.HasValue && herpesStrength <= 0.0f && aggregate - amount > 25.0f && aggregate <= 25.0f)
{
GameServer.Log($"{GameServer.ClientLogName(client)} has lost more than 25 karma in the past minute.", ServerLog.MessageType.Karma);
}
}
if (TestMode)
{
SendKarmaNotifications(client, debugKarmaChangeReason);
}
}
}
}