using System.Linq; using Content.Server.NPC.Components; using Robust.Shared.Prototypes; namespace Content.Server.NPC.Systems { /// /// Outlines faction relationships with each other. /// public sealed class FactionSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly EntityLookupSystem _lookup = 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) { RefreshFactions(); } private void OnFactionStartup(EntityUid uid, FactionComponent component, ComponentStartup args) { RefreshFactions(component); } /// /// Refreshes the cached factions for this component. /// private void RefreshFactions(FactionComponent component) { foreach (var faction in component.Factions) { // YAML Linter already yells about this if (!_factions.TryGetValue(faction, out var factionData)) continue; component.FriendlyFactions.UnionWith(factionData.Friendly); component.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); } } public IEnumerable GetNearbyHostiles(EntityUid entity, float range, FactionComponent? component = null) { if (!Resolve(entity, ref component, false)) return Array.Empty(); return GetNearbyFactions(entity, range, component.HostileFactions); } public IEnumerable GetNearbyFriendlies(EntityUid entity, float range, FactionComponent? 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 IsFriendly(EntityUid uidA, EntityUid uidB, FactionComponent? factionA = null, FactionComponent? 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); } /// /// 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(); } } }