From e8f4975f46ba1ff4c495c0a4e073ea9bed63618e Mon Sep 17 00:00:00 2001 From: Pok <113675512+Pok27@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:10:39 +0300 Subject: [PATCH] Changeling gaining DNA (#43759) * changeling-gain-DNA * review * Return IsUniqueDevour * oops * Update SharedChangelingIdentitySystem.cs * Store data in ChangelingIdentityData + fixes List of changes: - Whether an identity granted devour is now tracked in ChangelingIdentityData - Made devoured shutdown cleanup again - Made server set the mind to null if it never found one, instead of being invalid Fixes: - Fixed name being networked as empty if original was deleted --------- Co-authored-by: ScarKy0 --- .../Systems/ChangelingIdentitySystem.cs | 1 + .../Systems/ChangelingIdentitySystem.cs | 3 +- .../Components/ChangelingDevourComponent.cs | 21 ++++-- .../Components/ChangelingDevouredComponent.cs | 10 +-- .../Components/ChangelingIdentityComponent.cs | 9 +++ .../Components/RecentlyDevouredComponent.cs | 9 +++ .../Systems/ChangelingDevourSystem.cs | 70 ++++++++++++------- .../Systems/SharedChangelingIdentitySystem.cs | 45 +++++++----- 8 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs diff --git a/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs b/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs index 917eeefc08..1dbf2ad920 100644 --- a/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs +++ b/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs @@ -33,6 +33,7 @@ public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem OriginalJob = identity.OriginalJob, OriginalName = identity.OriginalName, Starting = identity.Starting, + GrantedDna = identity.GrantedDna, }; ent.Comp.ConsumedIdentities.Add(data); diff --git a/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs b/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs index 6f7556196c..2d2eb04348 100644 --- a/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs +++ b/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs @@ -24,8 +24,9 @@ public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem Identity = GetNetEntity(identity.Identity), Original = GetNetEntity(identity.Original), OriginalJob = identity.OriginalJob, - OriginalName = identity.Original != null ? Name(identity.Original.Value) : string.Empty, + OriginalName = identity.OriginalName, Starting = identity.Starting, + GrantedDna = identity.GrantedDna, }; sentIdentities.Add(netData); diff --git a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs index 1f8b3c81d3..425d64f4a9 100644 --- a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs @@ -1,6 +1,8 @@ using Content.Shared.Changeling.Systems; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Store; using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; @@ -125,6 +127,15 @@ public sealed partial class ChangelingDevourComponent : Component [DataField, AutoNetworkedField] public float DevourPreventionPercentageThreshold = 0.1f; + /// + /// DNA awarded for successfully devouring a new identity. + /// + [DataField, AutoNetworkedField] + public Dictionary DevourDnaReward = new() + { + { "ChangelingDNA", 10 } + }; + public override bool SendOnlyToOwner => true; } @@ -134,9 +145,10 @@ public sealed partial class ChangelingDevourComponent : Component /// The changeling devouring this entity. /// The entity that was devoured. /// Whether the changeling is going to be given the target's identity after devouring. -/// Whether this entity was eaten by the changeling before. +/// Whether the changeling has never had the identity of this target before. +/// Whether this devour has granted the changeling Dna. [ByRefEvent] -public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique); +public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique, bool GrantedDna); /// /// Event raised on an entity when devoured by a changeling. @@ -144,6 +156,7 @@ public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Dev /// The changeling devouring this entity. /// The entity that was devoured. /// Whether the changeling is going to be given the target's identity after devouring. -/// Whether this entity was eaten by the changeling before. +/// Whether the changeling has never had the identity of this target before. +/// Whether this devour has granted the changeling Dna. [ByRefEvent] -public record struct ChangelingGotDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique); +public record struct ChangelingGotDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique, bool GrantedDna); diff --git a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs index f25489fe1f..7e7fa94b14 100644 --- a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs @@ -4,18 +4,12 @@ namespace Content.Shared.Changeling.Components; /// /// Component used for marking entities devoured by a changeling. -/// Used to prevent granting the identity several times. +/// Used to track which changelings have an identity of this entity. +/// Used for cleanup purposes. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ChangelingDevouredComponent : Component { - /// - /// Whether this entity has been devoured recently. - /// Gets set back to False when the entity with this component becomes again. - /// - [DataField, AutoNetworkedField] - public bool Recent; - /// /// HashSet of all changelings that have devoured this entity. /// diff --git a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs index affec705ab..dcb65d5641 100644 --- a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs @@ -112,6 +112,12 @@ public sealed partial class ChangelingIdentityData [DataField] public bool Starting = false; + /// + /// Whether this identity has granted DNA after devour. + /// + [DataField] + public bool GrantedDna = false; + /// /// Convert to a string representation. This if for logging & debugging. This is not localized and should not be /// shown to players. @@ -143,4 +149,7 @@ public sealed partial class ChangelingNetworkedIdentityData [DataField] public bool Starting; + + [DataField] + public bool GrantedDna; } diff --git a/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs b/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs new file mode 100644 index 0000000000..a48565ee54 --- /dev/null +++ b/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling.Components; + +/// +/// Marker component for entities that were devoured recently and cannot be devoured again until revived. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class RecentlyDevouredComponent : Component; diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs index 6d07fb9378..63aad8b4d6 100644 --- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Administration.Logs; using Content.Shared.Armor; using Content.Shared.Atmos.Rotting; using Content.Shared.Changeling.Components; +using Content.Shared.Store; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; @@ -12,6 +13,7 @@ using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; +using Content.Shared.Store.Components; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; @@ -20,16 +22,17 @@ namespace Content.Shared.Changeling.Systems; public sealed class ChangelingDevourSystem : EntitySystem { + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedStoreSystem _store = default!; public override void Initialize() { @@ -167,17 +170,24 @@ public sealed class ChangelingDevourSystem : EntitySystem _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); - // If this entity has never been devoured before, it counts as unique. - var unique = !_changelingIdentitySystem.TryGetDataFromOriginal(ent.Owner, target, out _); + // A unique identity is separate from whether we have actually devoured this target before. + var uniqueIdentity = IsUniqueDevour(ent.Owner, target); + var willGrantDna = WillDevourGrantDna(ent.Owner, target); // Even if not unique, target is supposed to give us an identity if it is not currently in our identity list. var becomesIdentity = !HasIdentity(ent.Owner, target); - var ev = new ChangelingDevouredEvent(ent.Owner, target, becomesIdentity, unique); + var ev = new ChangelingDevouredEvent(ent.Owner, target, becomesIdentity, uniqueIdentity, willGrantDna); RaiseLocalEvent(ent, ref ev, true); // We broadcast the event to allow relevant objectives to update. - var devouredEv = new ChangelingGotDevouredEvent(ent.Owner, target, becomesIdentity, unique); + var devouredEv = new ChangelingGotDevouredEvent(ent.Owner, target, becomesIdentity, uniqueIdentity, willGrantDna); RaiseLocalEvent(target, ref devouredEv); // Don't broadcast this one, all neccessary data is in the previous event already. Just use that one if a broadcast is needed. + + EnsureComp(target); + + // Grants the DNA reward associated with a successful unique devour. + if (willGrantDna && TryComp(ent, out var store)) + _store.TryAddCurrency(ent.Comp.DevourDnaReward, ent.Owner, store); } /// @@ -191,17 +201,6 @@ public sealed class ChangelingDevourSystem : EntitySystem return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null; } - /// - /// Has this entity been devoured by a changeling already before getting revived? - /// - public bool WasDevouredRecently(Entity entity) - { - if (!Resolve(entity, ref entity.Comp, false)) - return false; - - return entity.Comp.Recent; - } - /// /// Can the given changeling devour the given victim? /// @@ -220,7 +219,7 @@ public sealed class ChangelingDevourSystem : EntitySystem return false; } - if (WasDevouredRecently(victim)) + if (HasComp(victim)) { if (showPopup) _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-devoured-recently"), changeling.Owner, changeling.Owner, PopupType.Medium); @@ -282,16 +281,35 @@ public sealed class ChangelingDevourSystem : EntitySystem } /// - /// Checks whether this changeling has devoured the target entity at any point before. + /// Checks whether devouring this target has never been devoured by the changeling before. /// /// The changeling. /// The target entity. - /// True if target was previously devoured, False otherwise. + /// True if the target was never devoured before, otherwise False. public bool IsUniqueDevour(Entity ent, EntityUid devoured) { if (!Resolve(ent, ref ent.Comp, false)) return false; - return _changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _); + return !_changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _); + } + + /// + /// Checks whether devouring this entity will grant DNA to the changeling. + /// + /// The changeling. + /// The target entity. + /// True if this target entity has granted the changeling DNA before, False otherwise. + public bool WillDevourGrantDna(Entity ent, EntityUid devoured) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + // This target was never devoured, so obviously it can grant us DNA. + if (!_changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out var data)) + return true; + + // If the entity was Devoured, it means it already granted DNA, so we return False. + return !data.GrantedDna; } } diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index 307c6a2132..a6c1379a7a 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -37,23 +37,21 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem SubscribeLocalEvent(OnStoredRemove); SubscribeLocalEvent(OnDevouredShutdown); - SubscribeLocalEvent(OnDevouredMobState); + SubscribeLocalEvent(OnRecentlyDevouredMobState); } private void OnDevouredEntity(Entity ent, ref ChangelingDevouredEvent args) { - // We're not supposed to be given an identity. - if (!args.ObtainedIdentity) - return; + if (args.ObtainedIdentity) + { + CloneToPausedMap(ent, args.Devoured); + AddDevouredReference(ent, args.Devoured); + } - CloneToPausedMap(ent, args.Devoured); - - // We add a reference to ourselves to prevent repeated identity gain. - var targetDevoured = EnsureComp(args.Devoured); - targetDevoured.DevouredBy.Add(ent.Owner); - targetDevoured.Recent = true; - Dirty(args.Devoured, targetDevoured); - Dirty(ent); + if (args.GrantedDna && TryGetDataFromOriginal(ent.AsNullable(), args.Devoured, out var data)) + { + data.GrantedDna = true; + } } private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) @@ -75,6 +73,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return; data.Starting = true; + data.GrantedDna = true; // I have no idea how you're supposed to ever get DNA from yourself, but just in case. ent.Comp.CurrentIdentity = data.Identity; } @@ -100,14 +99,13 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem } } - private void OnDevouredMobState(Entity ent, ref MobStateChangedEvent args) + private void OnRecentlyDevouredMobState(Entity ent, ref MobStateChangedEvent args) { - // Once we are revived the body is no longer "recent". + // Once we are revived the body is no longer recently devoured. if (args.NewMobState != MobState.Alive) return; - ent.Comp.Recent = false; - Dirty(ent); + RemCompDeferred(ent); } private void OnStoredRemove(Entity ent, ref ComponentRemove args) @@ -222,6 +220,17 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return clone; } + /// + /// Marks that the changeling has successfully devoured the target. + /// + public void AddDevouredReference(Entity ent, EntityUid target) + { + var targetDevoured = EnsureComp(target); + targetDevoured.DevouredBy.Add(ent.Owner); + + Dirty(target, targetDevoured); + } + /// /// Drop a stored identity from the changeling's storage. /// @@ -348,10 +357,10 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem { data.Identity = identity; data.Original = original; - data.OriginalName = Name(identity); + data.OriginalName = Name(original); var foundMind = _mind.TryGetMind(original, out var mindId, out _); - data.OriginalMind = mindId; + data.OriginalMind = foundMind ? mindId : null; if (foundMind) {