NpcFactionSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. using Content.Shared.NPC.Components;
  2. using Content.Shared.NPC.Prototypes;
  3. using Robust.Shared.Prototypes;
  4. using System.Collections.Frozen;
  5. using System.Linq;
  6. namespace Content.Shared.NPC.Systems;
  7. /// <summary>
  8. /// Outlines faction relationships with each other.
  9. /// </summary>
  10. public sealed partial class NpcFactionSystem : EntitySystem
  11. {
  12. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  13. [Dependency] private readonly IPrototypeManager _proto = default!;
  14. [Dependency] private readonly SharedTransformSystem _xform = default!;
  15. /// <summary>
  16. /// To avoid prototype mutability we store an intermediary data class that gets used instead.
  17. /// </summary>
  18. private FrozenDictionary<string, FactionData> _factions = FrozenDictionary<string, FactionData>.Empty;
  19. public override void Initialize()
  20. {
  21. base.Initialize();
  22. SubscribeLocalEvent<NpcFactionMemberComponent, ComponentStartup>(OnFactionStartup);
  23. SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnProtoReload);
  24. InitializeException();
  25. RefreshFactions();
  26. }
  27. private void OnProtoReload(PrototypesReloadedEventArgs obj)
  28. {
  29. if (obj.WasModified<NpcFactionPrototype>())
  30. RefreshFactions();
  31. }
  32. private void OnFactionStartup(Entity<NpcFactionMemberComponent> ent, ref ComponentStartup args)
  33. {
  34. RefreshFactions(ent);
  35. }
  36. /// <summary>
  37. /// Refreshes the cached factions for this component.
  38. /// </summary>
  39. private void RefreshFactions(Entity<NpcFactionMemberComponent> ent)
  40. {
  41. ent.Comp.FriendlyFactions.Clear();
  42. ent.Comp.HostileFactions.Clear();
  43. foreach (var faction in ent.Comp.Factions)
  44. {
  45. // YAML Linter already yells about this, don't need to log an error here
  46. if (!_factions.TryGetValue(faction, out var factionData))
  47. continue;
  48. ent.Comp.FriendlyFactions.UnionWith(factionData.Friendly);
  49. ent.Comp.HostileFactions.UnionWith(factionData.Hostile);
  50. }
  51. // Add additional factions if it is written in prototype
  52. if (ent.Comp.AddFriendlyFactions != null)
  53. {
  54. ent.Comp.FriendlyFactions.UnionWith(ent.Comp.AddFriendlyFactions);
  55. }
  56. if (ent.Comp.AddHostileFactions != null)
  57. {
  58. ent.Comp.HostileFactions.UnionWith(ent.Comp.AddHostileFactions);
  59. }
  60. }
  61. /// <summary>
  62. /// Returns whether an entity is a member of a faction.
  63. /// </summary>
  64. public bool IsMember(Entity<NpcFactionMemberComponent?> ent, string faction)
  65. {
  66. if (!Resolve(ent, ref ent.Comp, false))
  67. return false;
  68. return ent.Comp.Factions.Contains(faction);
  69. }
  70. /// <summary>
  71. /// Returns whether an entity is a member of any listed faction.
  72. /// If the list is empty this returns false.
  73. /// </summary>
  74. public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, IEnumerable<ProtoId<NpcFactionPrototype>> factions)
  75. {
  76. if (!Resolve(ent, ref ent.Comp, false))
  77. return false;
  78. foreach (var faction in factions)
  79. {
  80. if (ent.Comp.Factions.Contains(faction))
  81. return true;
  82. }
  83. return false;
  84. }
  85. /// <summary>
  86. /// Adds this entity to the particular faction.
  87. /// </summary>
  88. public void AddFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
  89. {
  90. if (!_proto.HasIndex<NpcFactionPrototype>(faction))
  91. {
  92. Log.Error($"Unable to find faction {faction}");
  93. return;
  94. }
  95. ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
  96. if (!ent.Comp.Factions.Add(faction))
  97. return;
  98. if (dirty)
  99. RefreshFactions((ent, ent.Comp));
  100. }
  101. /// <summary>
  102. /// Adds this entity to the particular faction.
  103. /// </summary>
  104. public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
  105. {
  106. ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
  107. foreach (var faction in factions)
  108. {
  109. if (!_proto.HasIndex(faction))
  110. {
  111. Log.Error($"Unable to find faction {faction}");
  112. continue;
  113. }
  114. ent.Comp.Factions.Add(faction);
  115. }
  116. if (dirty)
  117. RefreshFactions((ent, ent.Comp));
  118. }
  119. /// <summary>
  120. /// Removes this entity from the particular faction.
  121. /// </summary>
  122. public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
  123. {
  124. if (!_proto.HasIndex<NpcFactionPrototype>(faction))
  125. {
  126. Log.Error($"Unable to find faction {faction}");
  127. return;
  128. }
  129. if (!Resolve(ent, ref ent.Comp, false))
  130. return;
  131. if (!ent.Comp.Factions.Remove(faction))
  132. return;
  133. if (dirty)
  134. RefreshFactions((ent, ent.Comp));
  135. }
  136. /// <summary>
  137. /// Remove this entity from all factions.
  138. /// </summary>
  139. public void ClearFactions(Entity<NpcFactionMemberComponent?> ent, bool dirty = true)
  140. {
  141. if (!Resolve(ent, ref ent.Comp, false))
  142. return;
  143. ent.Comp.Factions.Clear();
  144. if (dirty)
  145. RefreshFactions((ent, ent.Comp));
  146. }
  147. public IEnumerable<EntityUid> GetNearbyHostiles(Entity<NpcFactionMemberComponent?, FactionExceptionComponent?> ent, float range)
  148. {
  149. if (!Resolve(ent, ref ent.Comp1, false))
  150. return Array.Empty<EntityUid>();
  151. var hostiles = GetNearbyFactions(ent, range, ent.Comp1.HostileFactions)
  152. // ignore mobs that have both hostile faction and the same faction,
  153. // otherwise having multiple factions is strictly negative
  154. .Where(target => !IsEntityFriendly((ent, ent.Comp1), target));
  155. if (!Resolve(ent, ref ent.Comp2, false))
  156. return hostiles;
  157. // ignore anything from enemy faction that we are explicitly friendly towards
  158. var faction = (ent.Owner, ent.Comp2);
  159. return hostiles
  160. .Union(GetHostiles(faction))
  161. .Where(target => !IsIgnored(faction, target));
  162. }
  163. public IEnumerable<EntityUid> GetNearbyFriendlies(Entity<NpcFactionMemberComponent?> ent, float range)
  164. {
  165. if (!Resolve(ent, ref ent.Comp, false))
  166. return Array.Empty<EntityUid>();
  167. return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions);
  168. }
  169. private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, HashSet<ProtoId<NpcFactionPrototype>> factions)
  170. {
  171. var xform = Transform(entity);
  172. foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(_xform.GetMapCoordinates((entity, xform)), range))
  173. {
  174. if (ent.Owner == entity)
  175. continue;
  176. if (!factions.Overlaps(ent.Comp.Factions))
  177. continue;
  178. yield return ent.Owner;
  179. }
  180. }
  181. /// <remarks>
  182. /// 1-way and purely faction based, ignores faction exception.
  183. /// </remarks>
  184. public bool IsEntityFriendly(Entity<NpcFactionMemberComponent?> ent, Entity<NpcFactionMemberComponent?> other)
  185. {
  186. if (!Resolve(ent, ref ent.Comp, false) || !Resolve(other, ref other.Comp, false))
  187. return false;
  188. return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions);
  189. }
  190. public bool IsFactionFriendly(string target, string with)
  191. {
  192. return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target);
  193. }
  194. public bool IsFactionFriendly(string target, Entity<NpcFactionMemberComponent?> with)
  195. {
  196. if (!Resolve(with, ref with.Comp, false))
  197. return false;
  198. return with.Comp.Factions.All(x => IsFactionFriendly(target, x)) ||
  199. with.Comp.FriendlyFactions.Contains(target);
  200. }
  201. public bool IsFactionHostile(string target, string with)
  202. {
  203. return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target);
  204. }
  205. public bool IsFactionHostile(string target, Entity<NpcFactionMemberComponent?> with)
  206. {
  207. if (!Resolve(with, ref with.Comp, false))
  208. return false;
  209. return with.Comp.Factions.All(x => IsFactionHostile(target, x)) ||
  210. with.Comp.HostileFactions.Contains(target);
  211. }
  212. public bool IsFactionNeutral(string target, string with)
  213. {
  214. return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with);
  215. }
  216. /// <summary>
  217. /// Makes the source faction friendly to the target faction, 1-way.
  218. /// </summary>
  219. public void MakeFriendly(string source, string target)
  220. {
  221. if (!_factions.TryGetValue(source, out var sourceFaction))
  222. {
  223. Log.Error($"Unable to find faction {source}");
  224. return;
  225. }
  226. if (!_factions.ContainsKey(target))
  227. {
  228. Log.Error($"Unable to find faction {target}");
  229. return;
  230. }
  231. sourceFaction.Friendly.Add(target);
  232. sourceFaction.Hostile.Remove(target);
  233. RefreshFactions();
  234. }
  235. /// <summary>
  236. /// Makes the source faction hostile to the target faction, 1-way.
  237. /// </summary>
  238. public void MakeHostile(string source, string target)
  239. {
  240. if (!_factions.TryGetValue(source, out var sourceFaction))
  241. {
  242. Log.Error($"Unable to find faction {source}");
  243. return;
  244. }
  245. if (!_factions.ContainsKey(target))
  246. {
  247. Log.Error($"Unable to find faction {target}");
  248. return;
  249. }
  250. sourceFaction.Friendly.Remove(target);
  251. sourceFaction.Hostile.Add(target);
  252. RefreshFactions();
  253. }
  254. private void RefreshFactions()
  255. {
  256. _factions = _proto.EnumeratePrototypes<NpcFactionPrototype>().ToFrozenDictionary(
  257. faction => faction.ID,
  258. faction => new FactionData
  259. {
  260. Friendly = faction.Friendly.ToHashSet(),
  261. Hostile = faction.Hostile.ToHashSet()
  262. });
  263. var query = AllEntityQuery<NpcFactionMemberComponent>();
  264. while (query.MoveNext(out var uid, out var comp))
  265. {
  266. comp.FriendlyFactions.Clear();
  267. comp.HostileFactions.Clear();
  268. RefreshFactions((uid, comp));
  269. }
  270. }
  271. }