Cloning Refactor and bugfixes (#35555)

* cloning refactor

* cleanup and fixes

* don't pick from 0

* give dwarves the correct species

* fix dna and bloodstream reagent data cloning

* don't copy helmets

* be less redundant
This commit is contained in:
slarticodefast
2025-03-02 16:50:12 +01:00
committed by GitHub
parent 02d3595faa
commit ceff2bea00
25 changed files with 796 additions and 420 deletions

View File

@@ -1,7 +1,6 @@
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.EntityEffects.Effects; using Content.Server.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!; [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume; bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
{
donorComp.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
RaiseLocalEvent(entity.Owner, ref ev);
}
// Fill blood solution with BLOOD // Fill blood solution with BLOOD
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume); bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
} }
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
reagentData.AddRange(GetEntityBloodData(entity.Owner)); reagentData.AddRange(GetEntityBloodData(entity.Owner));
} }
} }
else
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
} }
/// <summary> /// <summary>
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
var bloodData = new List<ReagentData>(); var bloodData = new List<ReagentData>();
var dnaData = new DnaData(); var dnaData = new DnaData();
if (TryComp<DnaComponent>(uid, out var donorComp)) if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
{
dnaData.DNA = donorComp.DNA; dnaData.DNA = donorComp.DNA;
} else else
{
dnaData.DNA = Loc.GetString("forensics-dna-unknown"); dnaData.DNA = Loc.GetString("forensics-dna-unknown");
}
bloodData.Add(dnaData); bloodData.Add(dnaData);

View File

@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
{ {
private readonly EntityUid _mindId; private readonly EntityUid _mindId;
private readonly MindComponent _mind; private readonly MindComponent _mind;
private readonly CloningSystem _cloningSystem; private readonly CloningPodSystem _cloningPodSystem;
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys) public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
{ {
_mindId = mindId; _mindId = mindId;
_mind = mind; _mind = mind;
_cloningSystem = cloningSys; _cloningPodSystem = cloningPodSys;
} }
public override void HandleMessage(EuiMessageBase msg) public override void HandleMessage(EuiMessageBase msg)
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
return; return;
} }
_cloningSystem.TransferMindToClone(_mindId, _mind); _cloningPodSystem.TransferMindToClone(_mindId, _mind);
Close(); Close();
} }
} }

View File

@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Cloning.Components; using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems; using Content.Server.DeviceLinking.Systems;
using Content.Server.Medical.Components; using Content.Server.Medical.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Shared.Cloning; using Content.Shared.Cloning;
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Power; using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
namespace Content.Server.Cloning namespace Content.Server.Cloning
{ {
[UsedImplicitly]
public sealed class CloningConsoleSystem : EntitySystem public sealed class CloningConsoleSystem : EntitySystem
{ {
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!; [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly CloningSystem _cloningSystem = default!; [Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
if (mind.UserId.HasValue == false || mind.Session == null) if (mind.UserId.HasValue == false || mind.Session == null)
return; return;
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier)) if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}."); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
} }

View File

@@ -0,0 +1,323 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.EUI;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Containers;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Cloning;
public sealed class CloningPodSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly EuiManager _euiManager = null!;
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
public const float EasyModeCloningCost = 0.7f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
{
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
_signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
}
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
{
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
!EntityManager.EntityExists(entity) ||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
mindComp.Mind != null)
return;
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
_mindSystem.UnVisit(mindId, mind);
ClonesWaitingForMind.Remove(mind);
}
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
{
if (clonedComponent.Parent == EntityUid.Invalid ||
!EntityManager.EntityExists(clonedComponent.Parent) ||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
{
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
return;
}
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
}
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
{
ent.Comp.ConnectedConsole = null;
}
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
{
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
return;
if (args.Anchored)
{
_cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
return;
}
_cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
}
private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
return;
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
}
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
return false; // Mind already has clone
ClonesWaitingForMind.Remove(mind);
}
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
return false; // Body controlled by mind is not dead
// Yes, we still need to track down the client because we need to open the Eui
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
return false;
var cloningCost = (int)Math.Round(physics.FixturesMass);
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
// biomass checks
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
if (biomassAmount < cloningCost)
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
return false;
}
// end of biomass checks
// genetic damage checks
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
{
var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
chance *= failChanceModifier;
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
if (_robustRandom.Prob(chance))
{
clonePod.FailedClone = true;
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
AddComp<ActiveCloningPodComponent>(uid);
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
return true;
}
}
// end of genetic damage checks
if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
return false;
}
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob.Value);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
AddComp<ActiveCloningPodComponent>(uid);
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
return;
if (!this.IsPowered(ent.Owner, EntityManager))
return;
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (HasComp<EmaggedComponent>(uid))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!HasComp<EmaggedComponent>(uid))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
}
}

View File

@@ -1,350 +1,123 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.EUI;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Humanoid; using Content.Server.Humanoid;
using Content.Server.Jobs; using Content.Shared.Administration.Logs;
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning; using Content.Shared.Cloning;
using Content.Shared.Damage; using Content.Shared.Cloning.Events;
using Content.Shared.DeviceLinking.Events; using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Mind; using Content.Shared.Inventory;
using Content.Shared.Mind.Components; using Content.Shared.NameModifier.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.StatusEffect;
using Content.Shared.Roles.Jobs; using Content.Shared.Whitelist;
using Robust.Server.Containers; using Robust.Shared.Map;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Cloning namespace Content.Server.Cloning;
/// <summary>
/// System responsible for making a copy of a humanoid's body.
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
/// </summary>
public sealed class CloningSystem : EntitySystem
{ {
public sealed class CloningSystem : EntitySystem [Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
/// </summary>
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
{ {
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!; clone = null;
[Dependency] private readonly IPlayerManager _playerManager = null!; if (!_prototype.TryIndex(settingsId, out var settings))
[Dependency] private readonly IPrototypeManager _prototype = default!; return false; // invalid settings
[Dependency] private readonly EuiManager _euiManager = null!;
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new(); if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
public const float EasyModeCloningCost = 0.7f; return false; // whatever body was to be cloned, was not a humanoid
public override void Initialize() if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
return false; // invalid species
var attemptEv = new CloningAttemptEvent(settings);
RaiseLocalEvent(original, ref attemptEv);
if (attemptEv.Cancelled && !settings.ForceCloning)
return false; // cannot clone, for example due to the unrevivable trait
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
_humanoidSystem.CloneAppearance(original, clone.Value);
var componentsToCopy = settings.Components;
// don't make status effects permanent
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
foreach (var componentName in componentsToCopy)
{ {
base.Initialize(); if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
{
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
_signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
}
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
{
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
!EntityManager.EntityExists(entity) ||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
mindComp.Mind != null)
return;
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
_mindSystem.UnVisit(mindId, mind);
ClonesWaitingForMind.Remove(mind);
}
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
{
if (clonedComponent.Parent == EntityUid.Invalid ||
!EntityManager.EntityExists(clonedComponent.Parent) ||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
{ {
EntityManager.RemoveComponent<BeingClonedComponent>(uid); Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
return; continue;
}
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
}
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
{
pod.ConnectedConsole = null;
}
private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
{
if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
return;
if (args.Anchored)
{
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
return;
}
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
}
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
return;
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
}
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
return false; // Mind already has clone
ClonesWaitingForMind.Remove(mind);
} }
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value)) if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
return false; // Body controlled by mind is not dead
// Yes, we still need to track down the client because we need to open the Eui
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
return false; // whatever body was to be cloned, was not a humanoid
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
return false;
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
return false;
var cloningCost = (int) Math.Round(physics.FixturesMass);
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
// biomass checks
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
if (biomassAmount < cloningCost)
{ {
if (clonePod.ConnectedConsole != null) if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false); RemComp(clone.Value, componentRegistration.Type);
return false; CopyComp(original, clone.Value, sourceComp);
}
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
// end of biomass checks
// genetic damage checks
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
{
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
chance *= failChanceModifier;
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
if (_robustRandom.Prob(chance))
{
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
clonePod.FailedClone = true;
AddComp<ActiveCloningPodComponent>(uid);
return true;
}
}
// end of genetic damage checks
var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
_humanoidSystem.CloneAppearance(bodyToClone, mob);
var ev = new CloningEvent(bodyToClone, mob);
RaiseLocalEvent(bodyToClone, ref ev);
if (!ev.NameHandled)
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
AddComp<ActiveCloningPodComponent>(uid);
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
// Add on special job components to the mob.
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
{
foreach (var special in prototype.Special)
{
if (special is AddComponentSpecial)
special.AfterEquip(mob);
}
}
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
} }
} }
/// <summary> var cloningEv = new CloningEvent(settings, clone.Value);
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew. RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args) // Add equipment first so that SetEntityName also renames the ID card.
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
var originalName = Name(original);
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
originalName = nameModComp.BaseName;
// This will properly set the BaseName and EntityName for the clone.
// Adding the component first before renaming will make sure RefreshNameModifers is called.
// Without this the name would get reverted to Urist.
// If the clone has no name modifiers, NameModifierComponent will be removed again.
EnsureComp<NameModifierComponent>(clone.Value);
_metaData.SetEntityName(clone.Value, originalName);
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
return true;
}
/// <summary>
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
return;
// Iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
while (slotEnumerator.NextItem(out var item, out var slot))
{ {
if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) // Spawn a copy of the item using the original prototype.
return; // This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
// we use a whitelist and blacklist to be sure to exclude any problematic entities
if (_emag.CheckFlag(uid, EmagType.Interaction)) if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
return; continue;
if (!this.IsPowered(uid, EntityManager)) var prototype = MetaData(item).EntityPrototype;
return; if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
} }
} }
} }

View File

@@ -0,0 +1,17 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.Cloning.Components;
/// <summary>
/// This is added to a marker entity in order to spawn a clone of a random player.
/// </summary>
[RegisterComponent, EntityCategory("Spawner")]
public sealed partial class RandomCloneSpawnerComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
}

View File

@@ -0,0 +1,47 @@
using Content.Server.Cloning.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Cloning;
/// <summary>
/// This deals with spawning and setting up a clone of a random crew member.
/// </summary>
public sealed class RandomCloneSpawnerSystem : EntitySystem
{
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
{
QueueDel(ent.Owner);
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
{
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
return;
}
var allHumans = _mind.GetAliveHumans();
if (allHumans.Count == 0)
return;
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
if (bodyToClone != null)
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.DoAfter; using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components; using Content.Server.Forensics.Components;
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract); SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit); SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit); // The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed); SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit); SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
@@ -65,18 +67,20 @@ namespace Content.Server.Forensics
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args) private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
{ {
ent.Comp.Fingerprint = GenerateFingerprint(); if (ent.Comp.Fingerprint == null)
Dirty(ent); RandomizeFingerprint((ent.Owner, ent.Comp));
} }
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args) private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
{ {
if (component.DNA == String.Empty) Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
if (ent.Comp.DNA == null)
RandomizeDNA((ent.Owner, ent.Comp));
else
{ {
component.DNA = GenerateDNA(); // If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA }; RaiseLocalEvent(ent.Owner, ref ev);
RaiseLocalEvent(uid, ref ev);
} }
} }
@@ -84,7 +88,7 @@ namespace Content.Server.Forensics
{ {
string dna = Loc.GetString("forensics-dna-unknown"); string dna = Loc.GetString("forensics-dna-unknown");
if (TryComp(uid, out DnaComponent? dnaComp)) if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
dna = dnaComp.DNA; dna = dnaComp.DNA;
foreach (EntityUid part in args.GibbedParts) foreach (EntityUid part in args.GibbedParts)
@@ -103,7 +107,7 @@ namespace Content.Server.Forensics
{ {
foreach (EntityUid hitEntity in args.HitEntities) foreach (EntityUid hitEntity in args.HitEntities)
{ {
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp)) if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
component.DNAs.Add(hitEntityComp.DNA); component.DNAs.Add(hitEntityComp.DNA);
} }
} }
@@ -301,6 +305,9 @@ namespace Content.Server.Forensics
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args) private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{ {
if (component.DNA == null)
return;
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient); var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA); recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned; recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -308,6 +315,36 @@ namespace Content.Server.Forensics
#region Public API #region Public API
/// <summary>
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
/// Does nothing if it does not have the DnaComponent.
/// </summary>
public void RandomizeDNA(Entity<DnaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.DNA = GenerateDNA();
Dirty(ent);
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
RaiseLocalEvent(ent.Owner, ref ev);
}
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
}
/// <summary> /// <summary>
/// Transfer DNA from one entity onto the forensics of another /// Transfer DNA from one entity onto the forensics of another
/// </summary> /// </summary>
@@ -316,7 +353,7 @@ namespace Content.Server.Forensics
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param> /// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{ {
if (TryComp<DnaComponent>(donor, out var donorComp)) if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{ {
EnsureComp<ForensicsComponent>(recipient, out var recipientComp); EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA); recipientComp.DNAs.Add(donorComp.DNA);

View File

@@ -216,17 +216,11 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species); var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid); _humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc. _metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
if (TryComp<DnaComponent>(ent, out var dna))
{
dna.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA }; // If the entity has the respecive components, then scramble the dna and fingerprint strings
RaiseLocalEvent(ent, ref ev); _forensicsSystem.RandomizeDNA(ent);
} _forensicsSystem.RandomizeFingerprint(ent);
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
{
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
}
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event _identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent); _popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);

View File

@@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped); SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
} }
// TODO: Turn this into a relay event.
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args) private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
{ {
// does the user already has this accent? // does the user already has this accent?

View File

@@ -0,0 +1,20 @@
using Content.Shared.Cloning.Events;
using Content.Shared.Traits.Assorted;
namespace Content.Server.Traits.Assorted;
public sealed class UnrevivableSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
}
private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
{
if (!ent.Comp.Cloneable)
args.Cancelled = true;
}
}

View File

@@ -24,11 +24,11 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Content.Shared.Prying.Components; using Content.Shared.Prying.Components;
@@ -58,8 +58,8 @@ public sealed partial class ZombieSystem
[Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly NPCSystem _npc = default!; [Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
/// <summary> /// <summary>
/// Handles an entity turning into a zombie when they die or go into crit /// Handles an entity turning into a zombie when they die or go into crit
@@ -235,7 +235,7 @@ public sealed partial class ZombieSystem
if (hasMind && _mind.TryGetSession(mindId, out var session)) if (hasMind && _mind.TryGetSession(mindId, out var session))
{ {
//Zombie role for player manifest //Zombie role for player manifest
_roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true); _role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
//Greeting message for new bebe zombers //Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting")); _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));

View File

@@ -5,18 +5,20 @@ using Content.Server.Chat;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Emoting.Systems; using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems; using Content.Server.Speech.EntitySystems;
using Content.Server.Roles;
using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Components;
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Cloning; using Content.Shared.Cloning.Events;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -38,7 +40,7 @@ namespace Content.Server.Zombies
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!; [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!; [Dependency] private readonly SharedRoleSystem _role = default!;
public const SlotFlags ProtectiveSlots = public const SlotFlags ProtectiveSlots =
SlotFlags.FEET | SlotFlags.FEET |
@@ -63,6 +65,8 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning); SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt); SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC); SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit); SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath); SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
@@ -272,7 +276,7 @@ namespace Content.Server.Zombies
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param> /// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
/// <param name="zombiecomp"></param> /// <param name="zombiecomp"></param>
/// <remarks> /// <remarks>
/// this currently only restore the name and skin/eye color from before zombified /// this currently only restore the skin/eye color from before zombified
/// TODO: completely rethink how zombies are done to allow reversal. /// TODO: completely rethink how zombies are done to allow reversal.
/// </remarks> /// </remarks>
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp) public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
@@ -292,14 +296,25 @@ namespace Content.Server.Zombies
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false); _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
_nameMod.RefreshNameModifiers(target);
return true; return true;
} }
private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args) private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
{ {
if (UnZombify(args.Source, args.Target, zombiecomp)) UnZombify(ent.Owner, args.CloneUid, ent.Comp);
args.NameHandled = true; }
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
{
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
}
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
{
_role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
} }
} }
} }

View File

@@ -0,0 +1,13 @@
namespace Content.Shared.Cloning.Events;
/// <summary>
/// Raised before a mob is cloned. Cancel to prevent cloning.
/// </summary>
[ByRefEvent]
public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
/// <summary>
/// Raised after a new mob got spawned when cloning a humanoid.
/// </summary>
[ByRefEvent]
public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);

View File

@@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
[RegisterComponent] [RegisterComponent]
public sealed partial class CloningPodComponent : Component public sealed partial class CloningPodComponent : Component
{ {
[ValidatePrototypeId<SinkPortPrototype>] [DataField]
public const string PodPort = "CloningPodReceiver"; public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
[ViewVariables] [ViewVariables]
public ContainerSlot BodyContainer = default!; public ContainerSlot BodyContainer = default!;
@@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
/// <summary> /// <summary>
/// The material that is used to clone entities. /// The material that is used to clone entities.
/// </summary> /// </summary>
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)] [DataField]
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass"; public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
/// <summary> /// <summary>
/// The current amount of time it takes to clone a body /// The current amount of time it takes to clone a body.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField]
public float CloningTime = 30f; public float CloningTime = 30f;
/// <summary> /// <summary>
/// The mob to spawn on emag /// The mob to spawn on emag.
/// </summary> /// </summary>
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)] [DataField]
public EntProtoId MobSpawnId = "MobAbomination"; public EntProtoId MobSpawnId = "MobAbomination";
// TODO: Remove this from here when cloning and/or zombies are refactored /// <summary>
[DataField("screamSound")] /// The sound played when a mob is spawned from an emagged cloning pod.
/// </summary>
[DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams") public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
{ {
Params = AudioParams.Default.WithVolume(4), Params = AudioParams.Default.WithVolume(4),
@@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
Gore, Gore,
NoMind NoMind
} }
/// <summary>
/// Raised after a new mob got spawned when cloning a humanoid
/// </summary>
[ByRefEvent]
public struct CloningEvent
{
public bool NameHandled = false;
public readonly EntityUid Source;
public readonly EntityUid Target;
public CloningEvent(EntityUid source, EntityUid target)
{
Source = source;
Target = target;
}
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.Inventory;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Content.Shared.Cloning;
/// <summary>
/// Settings for cloning a humanoid.
/// Used to decide which components should be copied.
/// </summary>
[Prototype]
public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
[ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
public string[]? Parents { get; }
[AbstractDataField]
[NeverPushInheritance]
public bool Abstract { get; }
/// <summary>
/// Determines if cloning can be prevented by traits etc.
/// </summary>
[DataField]
public bool ForceCloning = true;
/// <summary>
/// Which inventory slots will receive a copy of the original's clothing.
/// Disabled when null.
/// </summary>
[DataField]
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
/// <summary>
/// Whitelist for the equipment allowed to be copied.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for the equipment allowed to be copied.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
/// <summary>
/// Components to copy from the original to the clone.
/// This only makes a shallow copy of datafields!
/// If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
/// </summary>
[DataField]
[AlwaysPushInheritance]
public HashSet<string> Components = new();
}

View File

@@ -9,5 +9,5 @@ namespace Content.Shared.Forensics.Components;
public sealed partial class DnaComponent : Component public sealed partial class DnaComponent : Component
{ {
[DataField("dna"), AutoNetworkedField] [DataField("dna"), AutoNetworkedField]
public string DNA = String.Empty; public string? DNA;
} }

View File

@@ -53,7 +53,7 @@ public record struct TransferDnaEvent()
} }
/// <summary> /// <summary>
/// An event to generate and act upon new DNA for an entity. /// Raised on an entity when its DNA has been changed.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public record struct GenerateDnaEvent() public record struct GenerateDnaEvent()

View File

@@ -14,6 +14,12 @@ public sealed partial class UnrevivableComponent : Component
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool Analyzable = true; public bool Analyzable = true;
/// <summary>
/// Can this player be cloned using a cloning pod?
/// </summary>
[DataField, AutoNetworkedField]
public bool Cloneable = false;
/// <summary> /// <summary>
/// The loc string used to provide a reason for being unrevivable /// The loc string used to provide a reason for being unrevivable
/// </summary> /// </summary>

View File

@@ -26,5 +26,5 @@ cloning-console-component-msg-no-cloner = Not Ready: No Cloner Detected
cloning-console-component-msg-no-mind = Not Ready: No Soul Activity Detected cloning-console-component-msg-no-mind = Not Ready: No Soul Activity Detected
cloning-console-chat-error = ERROR: INSUFFICIENT BIOMASS. CLONING THIS BODY REQUIRES {$units} UNITS OF BIOMASS. cloning-console-chat-error = ERROR: INSUFFICIENT BIOMASS. CLONING THIS BODY REQUIRES {$units} UNITS OF BIOMASS.
cloning-console-uncloneable-trait-error = ERROR: SOUL IS ABSENT, CLONING IS IMPOSSIBLE. cloning-console-uncloneable-trait-error = ERROR: CLONING IS IMPOSSIBLE DUE TO ABNORMAL BODY COMPOSITION.
cloning-console-cellular-warning = WARNING: GENEFSCK CONFIDENCE SCORE IS {$percent}%. CLONING MAY HAVE UNEXPECTED RESULTS. cloning-console-cellular-warning = WARNING: GENEFSCK CONFIDENCE SCORE IS {$percent}%. CLONING MAY HAVE UNEXPECTED RESULTS.

View File

@@ -60,11 +60,13 @@
id: CloningPodSender id: CloningPodSender
name: signal-port-name-pod-receiver name: signal-port-name-pod-receiver
description: signal-port-description-pod-sender description: signal-port-description-pod-sender
defaultLinks: [ CloningPodReceiver ]
- type: sourcePort - type: sourcePort
id: MedicalScannerSender id: MedicalScannerSender
name: signal-port-name-med-scanner-sender name: signal-port-name-med-scanner-sender
description: signal-port-description-med-scanner-sender description: signal-port-description-med-scanner-sender
defaultLinks: [ MedicalScannerReceiver ]
- type: sourcePort - type: sourcePort
id: ArtifactAnalyzerSender id: ArtifactAnalyzerSender

View File

@@ -0,0 +1,84 @@
# Settings for cloning bodies
# If you add a new trait, job specific component or a component doing visual/examination changes for humanoids
# then add it here to the correct prototype.
# The datafields of the components are only shallow copied using CopyComp.
# Subscribe to CloningEvent instead if that is not enough.
- type: cloningSettings
id: BaseClone
components:
# general
- DetailExaminable
- Dna
- Fingerprint
- NpcFactionMember
# traits
# - LegsParalyzed (you get healed)
- LightweightDrunk
- Narcolepsy
- Pacified
- PainNumbness
- Paracusia
- PermanentBlindness
- Unrevivable
# job specific
- BibleUser
- CommandStaff
- Clumsy
- MindShield
- MimePowers
# accents
- Accentless
- BackwardsAccent
- BarkAccent
- BleatingAccent
- FrenchAccent
- GermanAccent
- LizardAccent
- MobsterAccent
- MonkeyAccent
- MothAccent
- MumbleAccent
- OwOAccent
- ParrotAccent
- PirateAccent
# - ReplacementAccent
# Not supported at the moment because AddAccentClothingComponent will make it permanent when cloned.
# TODO: AddAccentClothingComponent should use an inventory relay event.
# Also ZombieComponent overwrites the old replacement accent, because you can only have one at a time.
- RussianAccent
- ScrambledAccent
- SkeletonAccent
- SlurredAccent
- SouthernAccent
- SpanishAccent
- StutteringAccent
blacklist:
components:
- AttachedClothing # helmets, which are part of the suit
- type: cloningSettings
id: Antag
parent: BaseClone
components:
- HeadRevolutionary
- Revolutionary
- type: cloningSettings
id: CloningPod
parent: Antag
forceCloning: false
copyEquipment: null
# spawner
- type: entity
id: RandomCloneSpawner
name: Random Clone
suffix: Non-Antag
components:
- type: Sprite
sprite: Markers/paradox_clone.rsi
state: preview
- type: RandomCloneSpawner
settings: BaseClone

View File

@@ -8,7 +8,7 @@
- type: Hunger - type: Hunger
- type: Thirst - type: Thirst
- type: Icon - type: Icon
sprite: Mobs/Species/Slime/parts.rsi # It was like this beforehand, no idea why. sprite: Mobs/Species/Human/parts.rsi
state: full state: full
- type: Respirator - type: Respirator
damage: damage:
@@ -52,7 +52,7 @@
- type: Speech - type: Speech
speechSounds: Bass speechSounds: Bass
- type: HumanoidAppearance - type: HumanoidAppearance
species: Human species: Dwarf
hideLayersOnEquip: hideLayersOnEquip:
- Hair - Hair
- Snout - Snout

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "preview"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB