diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 3acc4ab180..6e9d8c4021 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -100,12 +100,10 @@ namespace Content.Client.Entry _prototypeManager.RegisterIgnore("seed"); // Seeds prototypes are server-only. _prototypeManager.RegisterIgnore("objective"); _prototypeManager.RegisterIgnore("holiday"); - _prototypeManager.RegisterIgnore("aiFaction"); _prototypeManager.RegisterIgnore("htnCompound"); _prototypeManager.RegisterIgnore("htnPrimitive"); _prototypeManager.RegisterIgnore("gameMap"); _prototypeManager.RegisterIgnore("gameMapPool"); - _prototypeManager.RegisterIgnore("npcFaction"); _prototypeManager.RegisterIgnore("lobbyBackground"); _prototypeManager.RegisterIgnore("advertisementsPack"); _prototypeManager.RegisterIgnore("gamePreset"); diff --git a/Content.Server/Dragon/Components/DragonComponent.cs b/Content.Server/Dragon/Components/DragonComponent.cs index f403979a00..80461e156a 100644 --- a/Content.Server/Dragon/Components/DragonComponent.cs +++ b/Content.Server/Dragon/Components/DragonComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.NPC.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -57,5 +58,12 @@ namespace Content.Server.Dragon { Params = AudioParams.Default.WithVolume(3f), }; + + /// + /// NPC faction to re-add after being zombified. + /// Prevents zombie dragon from being attacked by its own carp. + /// + [DataField] + public ProtoId Faction = "Dragon"; } } diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index 93d6bc8db0..36df6fb6f2 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -10,6 +10,8 @@ using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Movement.Systems; +using Content.Shared.NPC.Systems; +using Content.Shared.Zombies; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; @@ -24,6 +26,7 @@ public sealed partial class DragonSystem : EntitySystem [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDef = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly NpcFactionSystem _faction = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; @@ -55,6 +58,7 @@ public sealed partial class DragonSystem : EntitySystem SubscribeLocalEvent(OnDragonMove); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnCreated); + SubscribeLocalEvent(OnZombified); } public override void Update(float frameTime) @@ -202,6 +206,12 @@ public sealed partial class DragonSystem : EntitySystem }, mind); } + private void OnZombified(Entity ent, ref EntityZombifiedEvent args) + { + // prevent carp attacking zombie dragon + _faction.AddFaction(ent.Owner, ent.Comp.Faction); + } + private void Roar(EntityUid uid, DragonComponent comp) { if (comp.SoundRoar != null) diff --git a/Content.Server/Friends/Components/PettableFriendComponent.cs b/Content.Server/Friends/Components/PettableFriendComponent.cs deleted file mode 100644 index fe68029a66..0000000000 --- a/Content.Server/Friends/Components/PettableFriendComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Server.Friends.Systems; - -namespace Content.Server.Friends.Components; - -/// -/// Pet something to become friends with it (use in hand, press Z) -/// Uses FactionExceptionComponent behind the scenes -/// -[RegisterComponent, Access(typeof(PettableFriendSystem))] -public sealed partial class PettableFriendComponent : Component -{ - /// - /// Localized popup sent when petting for the first time - /// - [DataField("successString", required: true), ViewVariables(VVAccess.ReadWrite)] - public string SuccessString = string.Empty; - - /// - /// Localized popup sent when petting multiple times - /// - [DataField("failureString", required: true), ViewVariables(VVAccess.ReadWrite)] - public string FailureString = string.Empty; -} diff --git a/Content.Server/Friends/Systems/PettableFriendSystem.cs b/Content.Server/Friends/Systems/PettableFriendSystem.cs deleted file mode 100644 index 8f70c843e7..0000000000 --- a/Content.Server/Friends/Systems/PettableFriendSystem.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Content.Server.Friends.Components; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Interaction.Events; -using Content.Shared.Popups; - -namespace Content.Server.Friends.Systems; - -public sealed class PettableFriendSystem : EntitySystem -{ - [Dependency] private readonly NpcFactionSystem _factionException = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnUseInHand); - SubscribeLocalEvent(OnRehydrated); - } - - private void OnUseInHand(EntityUid uid, PettableFriendComponent comp, UseInHandEvent args) - { - var user = args.User; - if (args.Handled || !TryComp(uid, out var factionException)) - return; - - if (_factionException.IsIgnored(uid, user, factionException)) - { - _popup.PopupEntity(Loc.GetString(comp.FailureString, ("target", uid)), user, user); - return; - } - - // you have made a new friend :) - _popup.PopupEntity(Loc.GetString(comp.SuccessString, ("target", uid)), user, user); - _factionException.IgnoreEntity(uid, user, factionException); - args.Handled = true; - } - - private void OnRehydrated(EntityUid uid, PettableFriendComponent _, ref GotRehydratedEvent args) - { - // can only pet before hydrating, after that the fish cannot be negotiated with - if (!TryComp(uid, out var comp)) - return; - - var targetComp = AddComp(args.Target); - _factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp); - } -} diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 8efd61b469..c66a9d12a1 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -1,8 +1,8 @@ using Content.Server.Maps; -using Content.Server.NPC.Components; using Content.Server.RoundEnd; using Content.Server.StationEvents.Events; using Content.Shared.Dataset; +using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; using Robust.Shared.Map; using Robust.Shared.Prototypes; diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs index 62619db76a..e904d8a7c2 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -1,5 +1,5 @@ -using Content.Server.NPC.Components; using Content.Shared.Dataset; +using Content.Shared.NPC.Prototypes; using Content.Shared.Random; using Content.Shared.Roles; using Robust.Shared.Audio; diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index b9255bcbe4..ea5ab9c2ab 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -7,8 +7,6 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; using Content.Server.Mind; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; using Content.Server.Nuke; using Content.Server.NukeOps; using Content.Server.Popups; @@ -30,6 +28,8 @@ using Content.Shared.Humanoid.Prototypes; using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; using Content.Shared.Preferences; @@ -474,7 +474,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var eligibleQuery = EntityQueryEnumerator(); while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) { - if (!_npcFaction.IsFactionHostile(component.Faction, eligibleUid, member)) + if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) continue; eligible.Add((eligibleUid, eligibleComp, member)); diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs index 98926536b9..900554d824 100644 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs @@ -4,8 +4,6 @@ using Content.Server.Administration.Commands; using Content.Server.Cargo.Systems; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; using Content.Server.Preferences.Managers; using Content.Server.Spawners.Components; using Content.Server.Station.Components; @@ -14,6 +12,8 @@ using Content.Shared.CCVar; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Mind; +using Content.Shared.NPC.Prototypes; +using Content.Shared.NPC.Systems; using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Server.GameObjects; diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index d20775c734..5caa223c9c 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -4,8 +4,6 @@ using Content.Server.EUI; using Content.Server.Flash; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; -using Content.Server.NPC.Components; -using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Server.Revolutionary; using Content.Server.Revolutionary.Components; @@ -23,6 +21,8 @@ using Content.Shared.Mindshield.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Prototypes; +using Content.Shared.NPC.Systems; using Content.Shared.Revolutionary.Components; using Content.Shared.Roles; using Content.Shared.Stunnable; diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index fc9f0a9a9f..769d7e0a5b 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; -using Content.Server.NPC.Systems; using Content.Server.Objectives; using Content.Server.PDA.Ringer; using Content.Server.Roles; @@ -10,6 +9,7 @@ using Content.Shared.CCVar; using Content.Shared.Dataset; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Systems; using Content.Shared.Objectives.Components; using Content.Shared.PDA; using Content.Shared.Roles; diff --git a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs deleted file mode 100644 index 804a61b456..0000000000 --- a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Server.NPC.Systems; - -namespace Content.Server.NPC.Components; - -/// -/// This is used for tracking entities stored in -/// -[RegisterComponent, Access(typeof(NpcFactionSystem))] -public sealed partial class FactionExceptionTrackerComponent : Component -{ - /// - /// entities with that are tracking this entity. - /// - [DataField("entities")] - public HashSet Entities = new(); -} diff --git a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs deleted file mode 100644 index 72df5d0c8a..0000000000 --- a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Server.NPC.Systems; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; - -namespace Content.Server.NPC.Components -{ - [RegisterComponent] - [Access(typeof(NpcFactionSystem))] - public sealed partial class NpcFactionMemberComponent : Component - { - /// - /// Factions this entity is a part of. - /// - [ViewVariables(VVAccess.ReadWrite), - DataField("factions", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] - public HashSet Factions = new(); - - /// - /// Cached friendly factions. - /// - [ViewVariables] - public readonly HashSet FriendlyFactions = new(); - - /// - /// Cached hostile factions. - /// - [ViewVariables] - public readonly HashSet HostileFactions = new(); - } -} diff --git a/Content.Server/NPC/Components/NpcFactionPrototype.cs b/Content.Server/NPC/Components/NpcFactionPrototype.cs deleted file mode 100644 index fe5774710a..0000000000 --- a/Content.Server/NPC/Components/NpcFactionPrototype.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Server.NPC.Components -{ - /// - /// Contains data about this faction's relations with other factions. - /// - [Prototype("npcFaction")] - public sealed partial class NpcFactionPrototype : IPrototype - { - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; - - [ViewVariables(VVAccess.ReadWrite), DataField("friendly", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Friendly = new(); - - [ViewVariables(VVAccess.ReadWrite), DataField("hostile", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Hostile = new(); - } -} diff --git a/Content.Server/NPC/FactionData.cs b/Content.Server/NPC/FactionData.cs deleted file mode 100644 index b74150acc9..0000000000 --- a/Content.Server/NPC/FactionData.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Content.Server.NPC; - -/// -/// Cached data for the faction prototype. Can be modified at runtime. -/// -public sealed class FactionData -{ - [ViewVariables] - public HashSet Friendly = new(); - - [ViewVariables] - public HashSet Hostile = new(); -} diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs index cde8decefc..d6b2000f32 100644 --- a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs +++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs @@ -1,7 +1,9 @@ -using Content.Server.NPC.Components; +using Content.Server.NPC.Components; using Content.Shared.CombatMode; using Content.Shared.Damage; using Content.Shared.Mobs.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Robust.Shared.Collections; using Robust.Shared.Timing; @@ -22,37 +24,35 @@ public sealed class NPCRetaliationSystem : EntitySystem SubscribeLocalEvent(OnDisarmed); } - private void OnDamageChanged(EntityUid uid, NPCRetaliationComponent component, DamageChangedEvent args) + private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) { if (!args.DamageIncreased) return; - if (args.Origin is not { } origin) + if (args.Origin is not {} origin) return; - TryRetaliate(uid, origin, component); + TryRetaliate(ent, origin); } - private void OnDisarmed(EntityUid uid, NPCRetaliationComponent component, DisarmedEvent args) + private void OnDisarmed(Entity ent, ref DisarmedEvent args) { - TryRetaliate(uid, args.Source, component); + TryRetaliate(ent, args.Source); } - public bool TryRetaliate(EntityUid uid, EntityUid target, NPCRetaliationComponent? component = null) + public bool TryRetaliate(Entity ent, EntityUid target) { - if (!Resolve(uid, ref component)) - return false; - // don't retaliate against inanimate objects. if (!HasComp(target)) return false; - if (_npcFaction.IsEntityFriendly(uid, target)) + // don't retaliate against the same faction + if (_npcFaction.IsEntityFriendly(ent.Owner, target)) return false; - _npcFaction.AggroEntity(uid, target); - if (component.AttackMemoryLength is { } memoryLength) - component.AttackMemories[target] = _timing.CurTime + memoryLength; + _npcFaction.AggroEntity(ent.Owner, target); + if (ent.Comp.AttackMemoryLength is {} memoryLength) + ent.Comp.AttackMemories[target] = _timing.CurTime + memoryLength; return true; } @@ -64,12 +64,14 @@ public sealed class NPCRetaliationSystem : EntitySystem var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var retaliationComponent, out var factionException)) { + // TODO: can probably reuse this allocation and clear it foreach (var entity in new ValueList(retaliationComponent.AttackMemories.Keys)) { if (!TerminatingOrDeleted(entity) && _timing.CurTime < retaliationComponent.AttackMemories[entity]) continue; - _npcFaction.DeAggroEntity(uid, entity, factionException); + _npcFaction.DeAggroEntity((uid, factionException), entity); + // TODO: should probably remove the AttackMemory, thats the whole point of the ValueList right?? } } } diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index e7af2c9107..c58dc261fe 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -582,7 +582,7 @@ public sealed partial class NPCSteeringSystem (mask & otherBody.CollisionLayer) == 0x0 && (layer & otherBody.CollisionMask) == 0x0 || !_factionQuery.TryGetComponent(ent, out var otherFaction) || - !_npcFaction.IsEntityFriendly(uid, ent, ourFaction, otherFaction) || + !_npcFaction.IsEntityFriendly((uid, ourFaction), (ent, otherFaction)) || // Use <= 0 so we ignore stationary friends in case. Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f) { diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index c00375d648..f04dc56bc4 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -13,6 +13,8 @@ using Content.Shared.Interaction; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.NPC; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.NPC.Events; using Content.Shared.Physics; using Content.Shared.Weapons.Melee; diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 33941be929..6793161105 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Fluids.Components; using Content.Shared.Hands.Components; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; +using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Tools.Systems; using Content.Shared.Weapons.Melee; diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs deleted file mode 100644 index acef9005ea..0000000000 --- a/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Linq; -using Content.Server.NPC.Components; - -namespace Content.Server.NPC.Systems; - -/// -/// Prevents an NPC from attacking some entities from an enemy faction. -/// -public sealed partial class NpcFactionSystem -{ - private EntityQuery _exceptionQuery; - private EntityQuery _trackerQuery; - - public void InitializeException() - { - _exceptionQuery = GetEntityQuery(); - _trackerQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnTrackerShutdown); - } - - private void OnShutdown(EntityUid uid, FactionExceptionComponent component, ComponentShutdown args) - { - foreach (var ent in component.Hostiles) - { - if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) - continue; - trackerComponent.Entities.Remove(uid); - } - - foreach (var ent in component.Ignored) - { - if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) - continue; - trackerComponent.Entities.Remove(uid); - } - } - - private void OnTrackerShutdown(EntityUid uid, FactionExceptionTrackerComponent component, ComponentShutdown args) - { - foreach (var ent in component.Entities) - { - if (!_exceptionQuery.TryGetComponent(ent, out var exceptionComponent)) - continue; - exceptionComponent.Ignored.Remove(uid); - exceptionComponent.Hostiles.Remove(uid); - } - } - - /// - /// Returns whether the entity from an enemy faction won't be attacked - /// - public bool IsIgnored(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return false; - - return comp.Ignored.Contains(target); - } - - /// - /// Returns the specific hostile entities for a given entity. - /// - public IEnumerable GetHostiles(EntityUid uid, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return Array.Empty(); - - return comp.Hostiles; - } - - /// - /// Prevents an entity from an enemy faction from being attacked - /// - public void IgnoreEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp(uid); - comp.Ignored.Add(target); - EnsureComp(target).Entities.Add(uid); - } - - /// - /// Prevents a list of entities from an enemy faction from being attacked - /// - public void IgnoreEntities(EntityUid uid, IEnumerable ignored, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp(uid); - foreach (var ignore in ignored) - { - IgnoreEntity(uid, ignore, comp); - } - } - - /// - /// Makes an entity always be considered hostile. - /// - public void AggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp(uid); - comp.Hostiles.Add(target); - EnsureComp(target).Entities.Add(uid); - } - - /// - /// Makes an entity no longer be considered hostile, if it was. - /// Doesn't apply to regular faction hostilities. - /// - public void DeAggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return; - if (!comp.Hostiles.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker)) - return; - tracker.Entities.Remove(uid); - } - - /// - /// Makes a list of entities no longer be considered hostile, if it was. - /// Doesn't apply to regular faction hostilities. - /// - public void AggroEntities(EntityUid uid, IEnumerable entities, FactionExceptionComponent? comp = null) - { - comp ??= EnsureComp(uid); - foreach (var ent in entities) - { - AggroEntity(uid, ent, comp); - } - } -} diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index a4bd234307..8ae10d7383 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -25,6 +25,8 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Systems; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; @@ -215,11 +217,7 @@ namespace Content.Server.Zombies _damageable.SetAllDamage(target, damageablecomp, 0); _mobState.ChangeMobState(target, MobState.Alive); - var factionComp = EnsureComp(target); - foreach (var id in new List(factionComp.Factions)) - { - _faction.RemoveFaction(target, id); - } + _faction.ClearFactions(target, dirty: false); _faction.AddFaction(target, "Zombie"); //gives it the funny "Zombie ___" name. diff --git a/Content.Shared/Friends/Components/PettableFriendComponent.cs b/Content.Shared/Friends/Components/PettableFriendComponent.cs new file mode 100644 index 0000000000..d05e1769ae --- /dev/null +++ b/Content.Shared/Friends/Components/PettableFriendComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Friends.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Friends.Components; + +/// +/// Pet something to become friends with it (use in hand, press Z) +/// Requires this entity to have FactionExceptionComponent to work. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(PettableFriendSystem))] +public sealed partial class PettableFriendComponent : Component +{ + /// + /// Localized popup sent when petting for the first time + /// + [DataField(required: true)] + public LocId SuccessString = string.Empty; + + /// + /// Localized popup sent when petting multiple times + /// + [DataField(required: true)] + public LocId FailureString = string.Empty; +} diff --git a/Content.Shared/Friends/Systems/PettableFriendSystem.cs b/Content.Shared/Friends/Systems/PettableFriendSystem.cs new file mode 100644 index 0000000000..00a4ddd155 --- /dev/null +++ b/Content.Shared/Friends/Systems/PettableFriendSystem.cs @@ -0,0 +1,62 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Friends.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; +using Content.Shared.Popups; +using Content.Shared.Timing; + +namespace Content.Shared.Friends.Systems; + +public sealed class PettableFriendSystem : EntitySystem +{ + [Dependency] private readonly NpcFactionSystem _factionException = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + private EntityQuery _exceptionQuery; + private EntityQuery _useDelayQuery; + + public override void Initialize() + { + base.Initialize(); + + _exceptionQuery = GetEntityQuery(); + _useDelayQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnRehydrated); + } + + private void OnUseInHand(Entity ent, ref UseInHandEvent args) + { + var (uid, comp) = ent; + var user = args.User; + if (args.Handled || !_exceptionQuery.TryGetComponent(uid, out var exceptionComp)) + return; + + if (_useDelayQuery.TryGetComponent(uid, out var useDelay) && !_useDelay.TryResetDelay((uid, useDelay), true)) + return; + + var exception = (uid, exceptionComp); + if (_factionException.IsIgnored(exception, user)) + { + _popup.PopupClient(Loc.GetString(comp.FailureString, ("target", uid)), user, user); + return; + } + + // you have made a new friend :) + _popup.PopupClient(Loc.GetString(comp.SuccessString, ("target", uid)), user, user); + _factionException.IgnoreEntity(exception, user); + args.Handled = true; + } + + private void OnRehydrated(Entity ent, ref GotRehydratedEvent args) + { + // can only pet before hydrating, after that the fish cannot be negotiated with + if (!TryComp(ent, out var comp)) + return; + + _factionException.IgnoreEntities(args.Target, comp.Ignored); + } +} diff --git a/Content.Server/NPC/Components/FactionExceptionComponent.cs b/Content.Shared/NPC/Components/FactionExceptionComponent.cs similarity index 72% rename from Content.Server/NPC/Components/FactionExceptionComponent.cs rename to Content.Shared/NPC/Components/FactionExceptionComponent.cs index 6abd503537..54de0404c2 100644 --- a/Content.Server/NPC/Components/FactionExceptionComponent.cs +++ b/Content.Shared/NPC/Components/FactionExceptionComponent.cs @@ -1,23 +1,24 @@ -using Content.Server.NPC.Systems; +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; -namespace Content.Server.NPC.Components; +namespace Content.Shared.NPC.Components; /// /// Prevents an NPC from attacking ignored entities from enemy factions. /// Can be added to if pettable, see PettableFriendComponent. /// -[RegisterComponent, Access(typeof(NpcFactionSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] public sealed partial class FactionExceptionComponent : Component { /// /// Collection of entities that this NPC will refuse to attack /// - [DataField("ignored")] + [DataField] public HashSet Ignored = new(); /// /// Collection of entities that this NPC will attack, regardless of faction. /// - [DataField("hostiles")] + [DataField] public HashSet Hostiles = new(); } diff --git a/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs new file mode 100644 index 0000000000..f6eded7371 --- /dev/null +++ b/Content.Shared/NPC/Components/FactionExceptionTrackerComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.NPC.Components; + +/// +/// This is used for tracking entities stored in . +/// +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] +public sealed partial class FactionExceptionTrackerComponent : Component +{ + /// + /// Entities with that are tracking this entity. + /// + [DataField] + public HashSet Entities = new(); +} diff --git a/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs b/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs new file mode 100644 index 0000000000..91521e9854 --- /dev/null +++ b/Content.Shared/NPC/Components/NpcFactionMemberComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.NPC.Prototypes; +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.NPC.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSystem))] +public sealed partial class NpcFactionMemberComponent : Component +{ + /// + /// Factions this entity is a part of. + /// + [DataField] + public HashSet> Factions = new(); + + /// + /// Cached friendly factions. + /// + [ViewVariables] + public readonly HashSet> FriendlyFactions = new(); + + /// + /// Cached hostile factions. + /// + [ViewVariables] + public readonly HashSet> HostileFactions = new(); +} diff --git a/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs new file mode 100644 index 0000000000..1dcdd751c8 --- /dev/null +++ b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs @@ -0,0 +1,32 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.NPC.Prototypes; + +/// +/// Contains data about this faction's relations with other factions. +/// +[Prototype("npcFaction")] +public sealed partial class NpcFactionPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public List> Friendly = new(); + + [DataField] + public List> Hostile = new(); +} + +/// +/// Cached data for the faction prototype. Is modified at runtime, whereas the prototype is not. +/// +public record struct FactionData +{ + [ViewVariables] + public HashSet> Friendly; + + [ViewVariables] + public HashSet> Hostile; +} diff --git a/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs new file mode 100644 index 0000000000..e69f0c2f7a --- /dev/null +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs @@ -0,0 +1,135 @@ +using Content.Shared.NPC.Components; +using System.Linq; + +namespace Content.Shared.NPC.Systems; + +/// +/// Prevents an NPC from attacking some entities from an enemy faction. +/// Also makes it attack some entities even if they are in neutral factions (retaliation). +/// +public sealed partial class NpcFactionSystem +{ + private EntityQuery _exceptionQuery; + private EntityQuery _trackerQuery; + + public void InitializeException() + { + _exceptionQuery = GetEntityQuery(); + _trackerQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnTrackerShutdown); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + foreach (var uid in ent.Comp.Hostiles) + { + if (_trackerQuery.TryGetComponent(uid, out var tracker)) + tracker.Entities.Remove(ent); + } + + foreach (var uid in ent.Comp.Ignored) + { + if (_trackerQuery.TryGetComponent(uid, out var tracker)) + tracker.Entities.Remove(ent); + } + } + + private void OnTrackerShutdown(Entity ent, ref ComponentShutdown args) + { + foreach (var uid in ent.Comp.Entities) + { + if (!_exceptionQuery.TryGetComponent(uid, out var exception)) + continue; + + exception.Ignored.Remove(ent); + exception.Hostiles.Remove(ent); + } + } + + /// + /// Returns whether the entity from an enemy faction won't be attacked + /// + public bool IsIgnored(Entity ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return ent.Comp.Ignored.Contains(target); + } + + /// + /// Returns the specific hostile entities for a given entity. + /// + public IEnumerable GetHostiles(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return Array.Empty(); + + // evil c# + return ent.Comp!.Hostiles; + } + + /// + /// Prevents an entity from an enemy faction from being attacked + /// + public void IgnoreEntity(Entity ent, Entity target) + { + ent.Comp ??= EnsureComp(ent); + ent.Comp.Ignored.Add(target); + target.Comp ??= EnsureComp(target); + target.Comp.Entities.Add(ent); + } + + /// + /// Prevents a list of entities from an enemy faction from being attacked + /// + public void IgnoreEntities(Entity ent, IEnumerable ignored) + { + ent.Comp ??= EnsureComp(ent); + foreach (var ignore in ignored) + { + IgnoreEntity(ent, ignore); + } + } + + /// + /// Makes an entity always be considered hostile. + /// + public void AggroEntity(Entity ent, Entity target) + { + ent.Comp ??= EnsureComp(ent); + ent.Comp.Hostiles.Add(target); + target.Comp ??= EnsureComp(target); + target.Comp.Entities.Add(ent); + } + + /// + /// Makes an entity no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// + public void DeAggroEntity(Entity ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + if (!ent.Comp.Hostiles.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker)) + return; + + tracker.Entities.Remove(ent); + } + + /// + /// Makes a list of entities no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// + public void AggroEntities(Entity ent, IEnumerable entities) + { + ent.Comp ??= EnsureComp(ent); + foreach (var uid in entities) + { + AggroEntity(ent, uid); + } + } +} diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.cs similarity index 50% rename from Content.Server/NPC/Systems/NpcFactionSystem.cs rename to Content.Shared/NPC/Systems/NpcFactionSystem.cs index 36ee3724a2..ad81f01e1d 100644 --- a/Content.Server/NPC/Systems/NpcFactionSystem.cs +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.cs @@ -1,10 +1,10 @@ +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Prototypes; +using Robust.Shared.Prototypes; using System.Collections.Frozen; using System.Linq; -using Content.Server.NPC.Components; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -namespace Content.Server.NPC.Systems; +namespace Content.Shared.NPC.Systems; /// /// Outlines faction relationships with each other. @@ -12,7 +12,8 @@ namespace Content.Server.NPC.Systems; public sealed partial class NpcFactionSystem : EntitySystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly IPrototypeManager _protoManager = 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. @@ -36,122 +37,126 @@ public sealed partial class NpcFactionSystem : EntitySystem RefreshFactions(); } - private void OnFactionStartup(EntityUid uid, NpcFactionMemberComponent memberComponent, ComponentStartup args) + private void OnFactionStartup(Entity ent, ref ComponentStartup args) { - RefreshFactions(memberComponent); + RefreshFactions(ent); } /// /// Refreshes the cached factions for this component. /// - private void RefreshFactions(NpcFactionMemberComponent memberComponent) + private void RefreshFactions(Entity ent) { - memberComponent.FriendlyFactions.Clear(); - memberComponent.HostileFactions.Clear(); + ent.Comp.FriendlyFactions.Clear(); + ent.Comp.HostileFactions.Clear(); - foreach (var faction in memberComponent.Factions) + foreach (var faction in ent.Comp.Factions) { - // YAML Linter already yells about this + // YAML Linter already yells about this, don't need to log an error here if (!_factions.TryGetValue(faction, out var factionData)) continue; - memberComponent.FriendlyFactions.UnionWith(factionData.Friendly); - memberComponent.HostileFactions.UnionWith(factionData.Hostile); + ent.Comp.FriendlyFactions.UnionWith(factionData.Friendly); + ent.Comp.HostileFactions.UnionWith(factionData.Hostile); } } + /// + /// Returns whether an entity is a member of a faction. + /// + public bool IsMember(Entity ent, string faction) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return ent.Comp.Factions.Contains(faction); + } + /// /// Adds this entity to the particular faction. /// - public void AddFaction(EntityUid uid, string faction, bool dirty = true) + public void AddFaction(Entity ent, string faction, bool dirty = true) { - if (!_protoManager.HasIndex(faction)) + if (!_proto.HasIndex(faction)) { Log.Error($"Unable to find faction {faction}"); return; } - var comp = EnsureComp(uid); - if (!comp.Factions.Add(faction)) + ent.Comp ??= EnsureComp(ent); + if (!ent.Comp.Factions.Add(faction)) return; if (dirty) - { - RefreshFactions(comp); - } + RefreshFactions((ent, ent.Comp)); } /// /// Removes this entity from the particular faction. /// - public void RemoveFaction(EntityUid uid, string faction, bool dirty = true) + public void RemoveFaction(Entity ent, string faction, bool dirty = true) { - if (!_protoManager.HasIndex(faction)) + if (!_proto.HasIndex(faction)) { Log.Error($"Unable to find faction {faction}"); return; } - if (!TryComp(uid, out var component)) + if (!Resolve(ent, ref ent.Comp, false)) return; - if (!component.Factions.Remove(faction)) + if (!ent.Comp.Factions.Remove(faction)) return; if (dirty) - { - RefreshFactions(component); - } + RefreshFactions((ent, ent.Comp)); } /// /// Remove this entity from all factions. /// - public void ClearFactions(EntityUid uid, bool dirty = true) + public void ClearFactions(Entity ent, bool dirty = true) { - if (!TryComp(uid, out var component)) + if (!Resolve(ent, ref ent.Comp, false)) return; - component.Factions.Clear(); + ent.Comp.Factions.Clear(); if (dirty) - RefreshFactions(component); + RefreshFactions((ent, ent.Comp)); } - public IEnumerable GetNearbyHostiles(EntityUid entity, float range, NpcFactionMemberComponent? component = null) + public IEnumerable GetNearbyHostiles(Entity ent, float range) { - if (!Resolve(entity, ref component, false)) + if (!Resolve(ent, ref ent.Comp1, 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 - .Union(GetHostiles(entity, factionException)) - .Where(target => !IsIgnored(entity, target, factionException)); - } + 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; - 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)); } - [PublicAPI] - public IEnumerable GetNearbyFriendlies(EntityUid entity, float range, NpcFactionMemberComponent? component = null) + public IEnumerable GetNearbyFriendlies(Entity ent, float range) { - if (!Resolve(entity, ref component, false)) + if (!Resolve(ent, ref ent.Comp, false)) return Array.Empty(); - return GetNearbyFactions(entity, range, component.FriendlyFactions); + return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions); } - private IEnumerable GetNearbyFactions(EntityUid entity, float range, HashSet factions) + private IEnumerable GetNearbyFactions(EntityUid entity, float range, HashSet> factions) { - var xformQuery = GetEntityQuery(); - - if (!xformQuery.TryGetComponent(entity, out var entityXform)) - yield break; - - foreach (var ent in _lookup.GetEntitiesInRange(entityXform.MapPosition, range)) + var xform = Transform(entity); + foreach (var ent in _lookup.GetEntitiesInRange(_xform.GetMapCoordinates((entity, xform)), range)) { if (ent.Owner == entity) continue; @@ -163,12 +168,15 @@ public sealed partial class NpcFactionSystem : EntitySystem } } - public bool IsEntityFriendly(EntityUid uidA, EntityUid uidB, NpcFactionMemberComponent? factionA = null, NpcFactionMemberComponent? factionB = null) + /// + /// 1-way and purely faction based, ignores faction exception. + /// + public bool IsEntityFriendly(Entity ent, Entity other) { - if (!Resolve(uidA, ref factionA, false) || !Resolve(uidB, ref factionB, false)) + if (!Resolve(ent, ref ent.Comp, false) || !Resolve(other, ref other.Comp, false)) return false; - return factionA.Factions.Overlaps(factionB.Factions) || factionA.FriendlyFactions.Overlaps(factionB.Factions); + return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions); } public bool IsFactionFriendly(string target, string with) @@ -176,13 +184,13 @@ public sealed partial class NpcFactionSystem : EntitySystem return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target); } - public bool IsFactionFriendly(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) + public bool IsFactionFriendly(string target, Entity with) { - if (!Resolve(with, ref factionWith, false)) + if (!Resolve(with, ref with.Comp, false)) return false; - return factionWith.Factions.All(x => IsFactionFriendly(target, x)) || - factionWith.FriendlyFactions.Contains(target); + return with.Comp.Factions.All(x => IsFactionFriendly(target, x)) || + with.Comp.FriendlyFactions.Contains(target); } public bool IsFactionHostile(string target, string with) @@ -190,13 +198,13 @@ public sealed partial class NpcFactionSystem : EntitySystem return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target); } - public bool IsFactionHostile(string target, EntityUid with, NpcFactionMemberComponent? factionWith = null) + public bool IsFactionHostile(string target, Entity with) { - if (!Resolve(with, ref factionWith, false)) + if (!Resolve(with, ref with.Comp, false)) return false; - return factionWith.Factions.All(x => IsFactionHostile(target, x)) || - factionWith.HostileFactions.Contains(target); + return with.Comp.Factions.All(x => IsFactionHostile(target, x)) || + with.Comp.HostileFactions.Contains(target); } public bool IsFactionNeutral(string target, string with) @@ -226,26 +234,6 @@ public sealed partial class NpcFactionSystem : EntitySystem RefreshFactions(); } - private void RefreshFactions() - { - - _factions = _protoManager.EnumeratePrototypes().ToFrozenDictionary( - faction => faction.ID, - faction => 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. /// @@ -267,5 +255,23 @@ public sealed partial class NpcFactionSystem : EntitySystem 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)); + } + } +}