using Content.Server.NPC.Components; using Robust.Shared.Prototypes; using System.Linq; namespace Content.Server.NPC.Systems; /// /// Outlines faction relationships with each other. /// public sealed class NpcFactionSystem : EntitySystem { [Dependency] private readonly FactionExceptionSystem _factionException = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; private ISawmill _sawmill = default!; /// /// To avoid prototype mutability we store an intermediary data class that gets used instead. /// private Dictionary _factions = new(); public override void Initialize() { base.Initialize(); _sawmill = Logger.GetSawmill("faction"); SubscribeLocalEvent(OnFactionStartup); _protoManager.PrototypesReloaded += OnProtoReload; RefreshFactions(); } public override void Shutdown() { base.Shutdown(); _protoManager.PrototypesReloaded -= OnProtoReload; } private void OnProtoReload(PrototypesReloadedEventArgs obj) { if (!obj.ByType.ContainsKey(typeof(NpcFactionPrototype))) return; RefreshFactions(); } private void OnFactionStartup(EntityUid uid, NpcFactionMemberComponent memberComponent, ComponentStartup args) { RefreshFactions(memberComponent); } /// /// Refreshes the cached factions for this component. /// private void RefreshFactions(NpcFactionMemberComponent memberComponent) { memberComponent.FriendlyFactions.Clear(); memberComponent.HostileFactions.Clear(); foreach (var faction in memberComponent.Factions) { // YAML Linter already yells about this if (!_factions.TryGetValue(faction, out var factionData)) continue; memberComponent.FriendlyFactions.UnionWith(factionData.Friendly); memberComponent.HostileFactions.UnionWith(factionData.Hostile); } } /// /// Adds this entity to the particular faction. /// public void AddFaction(EntityUid uid, string faction, bool dirty = true) { if (!_protoManager.HasIndex(faction)) { _sawmill.Error($"Unable to find faction {faction}"); return; } var comp = EnsureComp(uid); if (!comp.Factions.Add(faction)) return; if (dirty) { RefreshFactions(comp); } } /// /// Removes this entity from the particular faction. /// public void RemoveFaction(EntityUid uid, string faction, bool dirty = true) { if (!_protoManager.HasIndex(faction)) { _sawmill.Error($"Unable to find faction {faction}"); return; } if (!TryComp(uid, out var component)) return; if (!component.Factions.Remove(faction)) return; if (dirty) { RefreshFactions(component); } } /// /// Remove this entity from all factions. /// public void ClearFactions(EntityUid uid, bool dirty = true) { if (!TryComp(uid, out var component)) return; component.Factions.Clear(); if (dirty) RefreshFactions(component); } public IEnumerable GetNearbyHostiles(EntityUid entity, float range, NpcFactionMemberComponent? component = null) { if (!Resolve(entity, ref component, false)) return Array.Empty(); var hostiles = GetNearbyFactions(entity, range, component.HostileFactions); if (TryComp(entity, out var factionException)) { // ignore anything from enemy faction that we are explicitly friendly towards return hostiles.Where(target => !_factionException.IsIgnored(factionException, target)); } return hostiles; } public IEnumerable GetNearbyFriendlies(EntityUid entity, float range, NpcFactionMemberComponent? component = null) { if (!Resolve(entity, ref component, false)) return Array.Empty(); return GetNearbyFactions(entity, range, component.FriendlyFactions); } private IEnumerable GetNearbyFactions(EntityUid entity, float range, HashSet factions) { var xformQuery = GetEntityQuery(); if (!xformQuery.TryGetComponent(entity, out var entityXform)) yield break; foreach (var comp in _lookup.GetComponentsInRange(entityXform.MapPosition, range)) { if (comp.Owner == entity) continue; if (!factions.Overlaps(comp.Factions)) continue; yield return comp.Owner; } } public bool IsEntityFriendly(EntityUid uidA, EntityUid uidB, NpcFactionMemberComponent? factionA = null, NpcFactionMemberComponent? factionB = null) { if (!Resolve(uidA, ref factionA, false) || !Resolve(uidB, ref factionB, false)) return false; return factionA.Factions.Overlaps(factionB.Factions) || factionA.FriendlyFactions.Overlaps(factionB.Factions); } public bool IsFactionFriendly(string target, string with) { return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target); } public bool IsFactionFriendly(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) { if (!Resolve(with, ref factionWith, false)) return false; return factionWith.Factions.All(x => IsFactionFriendly(target, x)) || factionWith.FriendlyFactions.Contains(target); } public bool IsFactionHostile(string target, string with) { return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target); } public bool IsFactionHostile(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) { if (!Resolve(with, ref factionWith, false)) return false; return factionWith.Factions.All(x => IsFactionHostile(target, x)) || factionWith.HostileFactions.Contains(target); } public bool IsFactionNeutral(string target, string with) { return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with); } /// /// Makes the source faction friendly to the target faction, 1-way. /// public void MakeFriendly(string source, string target) { if (!_factions.TryGetValue(source, out var sourceFaction)) { _sawmill.Error($"Unable to find faction {source}"); return; } if (!_factions.ContainsKey(target)) { _sawmill.Error($"Unable to find faction {target}"); return; } sourceFaction.Friendly.Add(target); sourceFaction.Hostile.Remove(target); RefreshFactions(); } private void RefreshFactions() { _factions.Clear(); foreach (var faction in _protoManager.EnumeratePrototypes()) { _factions[faction.ID] = new FactionData() { Friendly = faction.Friendly.ToHashSet(), Hostile = faction.Hostile.ToHashSet(), }; } foreach (var comp in EntityQuery(true)) { comp.FriendlyFactions.Clear(); comp.HostileFactions.Clear(); RefreshFactions(comp); } } /// /// Makes the source faction hostile to the target faction, 1-way. /// public void MakeHostile(string source, string target) { if (!_factions.TryGetValue(source, out var sourceFaction)) { _sawmill.Error($"Unable to find faction {source}"); return; } if (!_factions.ContainsKey(target)) { _sawmill.Error($"Unable to find faction {target}"); return; } sourceFaction.Friendly.Remove(target); sourceFaction.Hostile.Add(target); RefreshFactions(); } }