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 <scarky0@onet.eu>
This commit is contained in:
Pok
2026-04-27 23:10:39 +03:00
committed by GitHub
parent 754317d013
commit e8f4975f46
8 changed files with 111 additions and 57 deletions
@@ -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);
@@ -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);
@@ -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;
/// <summary>
/// DNA awarded for successfully devouring a new identity.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<string, FixedPoint2> DevourDnaReward = new()
{
{ "ChangelingDNA", 10 }
};
public override bool SendOnlyToOwner => true;
}
@@ -134,9 +145,10 @@ public sealed partial class ChangelingDevourComponent : Component
/// <param name="Changeling">The changeling devouring this entity.</param>
/// <param name="Devoured">The entity that was devoured.</param>
/// <param name="ObtainedIdentity">Whether the changeling is going to be given the target's identity after devouring.</param>
/// <param name="Unique">Whether this entity was eaten by the changeling before.</param>
/// <param name="Unique">Whether the changeling has never had the identity of this target before.</param>
/// <param name="GrantedDna">Whether this devour has granted the changeling Dna.</param>
[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);
/// <summary>
/// Event raised on an entity when devoured by a changeling.
@@ -144,6 +156,7 @@ public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Dev
/// <param name="Changeling">The changeling devouring this entity.</param>
/// <param name="Devoured">The entity that was devoured.</param>
/// <param name="ObtainedIdentity">Whether the changeling is going to be given the target's identity after devouring.</param>
/// <param name="Unique">Whether this entity was eaten by the changeling before.</param>
/// <param name="Unique">Whether the changeling has never had the identity of this target before.</param>
/// <param name="GrantedDna">Whether this devour has granted the changeling Dna.</param>
[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);
@@ -4,18 +4,12 @@ namespace Content.Shared.Changeling.Components;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChangelingDevouredComponent : Component
{
/// <summary>
/// Whether this entity has been devoured recently.
/// Gets set back to False when the entity with this component becomes <see cref="MobState.Alive"/> again.
/// </summary>
[DataField, AutoNetworkedField]
public bool Recent;
/// <summary>
/// HashSet of all changelings that have devoured this entity.
/// </summary>
@@ -112,6 +112,12 @@ public sealed partial class ChangelingIdentityData
[DataField]
public bool Starting = false;
/// <summary>
/// Whether this identity has granted DNA after devour.
/// </summary>
[DataField]
public bool GrantedDna = false;
/// <summary>
/// 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;
}
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// Marker component for entities that were devoured recently and cannot be devoured again until revived.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class RecentlyDevouredComponent : Component;
@@ -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<RecentlyDevouredComponent>(target);
// Grants the DNA reward associated with a successful unique devour.
if (willGrantDna && TryComp<StoreComponent>(ent, out var store))
_store.TryAddCurrency(ent.Comp.DevourDnaReward, ent.Owner, store);
}
/// <summary>
@@ -191,17 +201,6 @@ public sealed class ChangelingDevourSystem : EntitySystem
return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null;
}
/// <summary>
/// Has this entity been devoured by a changeling already before getting revived?
/// </summary>
public bool WasDevouredRecently(Entity<ChangelingDevouredComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, false))
return false;
return entity.Comp.Recent;
}
/// <summary>
/// Can the given changeling devour the given victim?
/// </summary>
@@ -220,7 +219,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
return false;
}
if (WasDevouredRecently(victim))
if (HasComp<RecentlyDevouredComponent>(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
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ent">The changeling.</param>
/// <param name="devoured">The target entity.</param>
/// <returns>True if target was previously devoured, False otherwise.</returns>
/// <returns>True if the target was never devoured before, otherwise False.</returns>
public bool IsUniqueDevour(Entity<ChangelingIdentityComponent?> ent, EntityUid devoured)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
return _changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _);
return !_changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _);
}
/// <summary>
/// Checks whether devouring this entity will grant DNA to the changeling.
/// </summary>
/// <param name="ent">The changeling.</param>
/// <param name="devoured">The target entity.</param>
/// <returns>True if this target entity has granted the changeling DNA before, False otherwise.</returns>
public bool WillDevourGrantDna(Entity<ChangelingIdentityComponent?> 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;
}
}
@@ -37,23 +37,21 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
SubscribeLocalEvent<ChangelingStoredIdentityComponent, ComponentRemove>(OnStoredRemove);
SubscribeLocalEvent<ChangelingDevouredComponent, ComponentShutdown>(OnDevouredShutdown);
SubscribeLocalEvent<ChangelingDevouredComponent, MobStateChangedEvent>(OnDevouredMobState);
SubscribeLocalEvent<RecentlyDevouredComponent, MobStateChangedEvent>(OnRecentlyDevouredMobState);
}
private void OnDevouredEntity(Entity<ChangelingIdentityComponent> 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<ChangelingDevouredComponent>(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<ChangelingIdentityComponent> 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<ChangelingDevouredComponent> ent, ref MobStateChangedEvent args)
private void OnRecentlyDevouredMobState(Entity<RecentlyDevouredComponent> 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<RecentlyDevouredComponent>(ent);
}
private void OnStoredRemove(Entity<ChangelingStoredIdentityComponent> ent, ref ComponentRemove args)
@@ -222,6 +220,17 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
return clone;
}
/// <summary>
/// Marks that the changeling has successfully devoured the target.
/// </summary>
public void AddDevouredReference(Entity<ChangelingIdentityComponent> ent, EntityUid target)
{
var targetDevoured = EnsureComp<ChangelingDevouredComponent>(target);
targetDevoured.DevouredBy.Add(ent.Owner);
Dirty(target, targetDevoured);
}
/// <summary>
/// Drop a stored identity from the changeling's storage.
/// </summary>
@@ -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)
{