using Content.Shared.NPC.Components; using Content.Shared.NPC.Prototypes; using Robust.Shared.Prototypes; using System.Collections.Frozen; using System.Linq; namespace Content.Shared.NPC.Systems; /// /// Outlines faction relationships with each other. /// public sealed partial class NpcFactionSystem : EntitySystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedTransformSystem _xform = default!; /// /// To avoid prototype mutability we store an intermediary data class that gets used instead. /// private FrozenDictionary _factions = FrozenDictionary.Empty; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnFactionStartup); SubscribeLocalEvent(OnProtoReload); InitializeException(); RefreshFactions(); } private void OnProtoReload(PrototypesReloadedEventArgs obj) { if (obj.WasModified()) RefreshFactions(); } private void OnFactionStartup(Entity ent, ref ComponentStartup args) { RefreshFactions(ent); } /// /// Refreshes the cached factions for this component. /// private void RefreshFactions(Entity ent) { ent.Comp.FriendlyFactions.Clear(); ent.Comp.HostileFactions.Clear(); foreach (var faction in ent.Comp.Factions) { // YAML Linter already yells about this, don't need to log an error here if (!_factions.TryGetValue(faction, out var factionData)) continue; ent.Comp.FriendlyFactions.UnionWith(factionData.Friendly); ent.Comp.HostileFactions.UnionWith(factionData.Hostile); } // Add additional factions if it is written in prototype if (ent.Comp.AddFriendlyFactions != null) { ent.Comp.FriendlyFactions.UnionWith(ent.Comp.AddFriendlyFactions); } if (ent.Comp.AddHostileFactions != null) { ent.Comp.HostileFactions.UnionWith(ent.Comp.AddHostileFactions); } } /// /// Returns whether an entity is a member of a faction. /// public bool IsMember(Entity ent, [ForbidLiteral] string faction) { if (!Resolve(ent, ref ent.Comp, false)) return false; return ent.Comp.Factions.Contains(faction); } /// /// Returns whether an entity is a member of any listed faction. /// If the list is empty this returns false. /// public bool IsMemberOfAny(Entity ent, [ForbidLiteral] IEnumerable> factions) { if (!Resolve(ent, ref ent.Comp, false)) return false; foreach (var faction in factions) { if (ent.Comp.Factions.Contains(faction)) return true; } return false; } /// /// Adds this entity to the particular faction. /// public void AddFaction(Entity ent, [ForbidLiteral] string faction, bool dirty = true) { if (!_proto.HasIndex(faction)) { Log.Error($"Unable to find faction {faction}"); return; } ent.Comp ??= EnsureComp(ent); if (!ent.Comp.Factions.Add(faction)) return; if (dirty) RefreshFactions((ent, ent.Comp)); } /// /// Adds this entity to the particular faction. /// public void AddFactions(Entity ent, [ForbidLiteral] HashSet> factions, bool dirty = true) { ent.Comp ??= EnsureComp(ent); foreach (var faction in factions) { if (!_proto.HasIndex(faction)) { Log.Error($"Unable to find faction {faction}"); continue; } ent.Comp.Factions.Add(faction); } if (dirty) RefreshFactions((ent, ent.Comp)); } /// /// Removes this entity from the particular faction. /// public void RemoveFaction(Entity ent, [ForbidLiteral] string faction, bool dirty = true) { if (!_proto.HasIndex(faction)) { Log.Error($"Unable to find faction {faction}"); return; } if (!Resolve(ent, ref ent.Comp, false)) return; if (!ent.Comp.Factions.Remove(faction)) return; if (dirty) RefreshFactions((ent, ent.Comp)); } /// /// Remove this entity from all factions. /// public void ClearFactions(Entity ent, bool dirty = true) { if (!Resolve(ent, ref ent.Comp, false)) return; ent.Comp.Factions.Clear(); if (dirty) RefreshFactions((ent, ent.Comp)); } public IEnumerable GetNearbyHostiles(Entity ent, float range) { if (!Resolve(ent, ref ent.Comp1, false)) return Array.Empty(); var hostiles = GetNearbyFactions(ent, range, ent.Comp1.HostileFactions) // ignore mobs that have both hostile faction and the same faction, // otherwise having multiple factions is strictly negative .Where(target => !IsEntityFriendly((ent, ent.Comp1), target)); if (!Resolve(ent, ref ent.Comp2, false)) return hostiles; // ignore anything from enemy faction that we are explicitly friendly towards var faction = (ent.Owner, ent.Comp2); return hostiles .Union(GetHostiles(faction)) .Where(target => !IsIgnored(faction, target)); } public IEnumerable GetNearbyFriendlies(Entity ent, float range) { if (!Resolve(ent, ref ent.Comp, false)) return Array.Empty(); return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions); } private IEnumerable GetNearbyFactions(EntityUid entity, float range, [ForbidLiteral] HashSet> factions) { var xform = Transform(entity); foreach (var ent in _lookup.GetEntitiesInRange(_xform.GetMapCoordinates((entity, xform)), range)) { if (ent.Owner == entity) continue; if (!factions.Overlaps(ent.Comp.Factions)) continue; yield return ent.Owner; } } /// /// 1-way and purely faction based, ignores faction exception. /// public bool IsEntityFriendly(Entity ent, Entity other) { if (!Resolve(ent, ref ent.Comp, false) || !Resolve(other, ref other.Comp, false)) return false; return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions); } public bool IsFactionFriendly([ForbidLiteral] string target, [ForbidLiteral] string with) { return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target); } public bool IsFactionFriendly([ForbidLiteral] string target, Entity with) { if (!Resolve(with, ref with.Comp, false)) return false; return with.Comp.Factions.All(x => IsFactionFriendly(target, x)) || with.Comp.FriendlyFactions.Contains(target); } public bool IsFactionHostile([ForbidLiteral] string target, [ForbidLiteral] string with) { return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target); } public bool IsFactionHostile([ForbidLiteral] string target, Entity with) { if (!Resolve(with, ref with.Comp, false)) return false; return with.Comp.Factions.All(x => IsFactionHostile(target, x)) || with.Comp.HostileFactions.Contains(target); } public bool IsFactionNeutral([ForbidLiteral] string target, [ForbidLiteral] string with) { return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with); } /// /// Makes the source faction friendly to the target faction, 1-way. /// public void MakeFriendly([ForbidLiteral] string source, [ForbidLiteral] string target) { if (!_factions.TryGetValue(source, out var sourceFaction)) { Log.Error($"Unable to find faction {source}"); return; } if (!_factions.ContainsKey(target)) { Log.Error($"Unable to find faction {target}"); return; } sourceFaction.Friendly.Add(target); sourceFaction.Hostile.Remove(target); RefreshFactions(); } /// /// Makes the source faction hostile to the target faction, 1-way. /// public void MakeHostile([ForbidLiteral] string source, [ForbidLiteral] string target) { if (!_factions.TryGetValue(source, out var sourceFaction)) { Log.Error($"Unable to find faction {source}"); return; } if (!_factions.ContainsKey(target)) { Log.Error($"Unable to find faction {target}"); return; } sourceFaction.Friendly.Remove(target); sourceFaction.Hostile.Add(target); RefreshFactions(); } private void RefreshFactions() { _factions = _proto.EnumeratePrototypes().ToFrozenDictionary( faction => faction.ID, faction => new FactionData { Friendly = faction.Friendly.ToHashSet(), Hostile = faction.Hostile.ToHashSet() }); var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp)) { comp.FriendlyFactions.Clear(); comp.HostileFactions.Clear(); RefreshFactions((uid, comp)); } } }