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.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Chemistry.Components;
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
public override void Initialize()
{
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
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
// 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);
}
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
reagentData.AddRange(GetEntityBloodData(entity.Owner));
}
}
else
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
/// <summary>
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
var bloodData = new List<ReagentData>();
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;
} else
{
else
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
}
bloodData.Add(dnaData);

View File

@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
{
private readonly EntityUid _mindId;
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;
_mind = mind;
_cloningSystem = cloningSys;
_cloningPodSystem = cloningPodSys;
}
public override void HandleMessage(EuiMessageBase msg)
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
return;
}
_cloningSystem.TransferMindToClone(_mindId, _mind);
_cloningPodSystem.TransferMindToClone(_mindId, _mind);
Close();
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Medical.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Cloning;
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.Cloning
{
[UsedImplicitly]
public sealed class CloningConsoleSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = 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 MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
if (mind.UserId.HasValue == false || mind.Session == null)
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)}.");
}

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.Jobs;
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.Administration.Logs;
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.Cloning.Events;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles.Jobs;
using Robust.Server.Containers;
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 Content.Shared.Inventory;
using Content.Shared.NameModifier.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
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!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[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!;
clone = null;
if (!_prototype.TryIndex(settingsId, out var settings))
return false; // invalid settings
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
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();
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)
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
{
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
return;
}
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);
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
continue;
}
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<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 (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
return false;
}
_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);
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
RemComp(clone.Value, componentRegistration.Type);
CopyComp(original, clone.Value, sourceComp);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
var cloningEv = new CloningEvent(settings, clone.Value);
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
// 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))
return;
// Spawn a copy of the item using the original prototype.
// 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))
return;
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
continue;
if (!this.IsPowered(uid, EntityManager))
return;
_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();
var prototype = MetaData(item).EntityPrototype;
if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
}
}
}

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.Systems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
public override void Initialize()
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
// 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, MeleeHitEvent>(OnMeleeHit);
@@ -65,18 +67,20 @@ namespace Content.Server.Forensics
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
{
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
if (ent.Comp.Fingerprint == null)
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();
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
RaiseLocalEvent(uid, ref ev);
// 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 };
RaiseLocalEvent(ent.Owner, ref ev);
}
}
@@ -84,7 +88,7 @@ namespace Content.Server.Forensics
{
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;
foreach (EntityUid part in args.GibbedParts)
@@ -103,7 +107,7 @@ namespace Content.Server.Forensics
{
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);
}
}
@@ -301,6 +305,9 @@ namespace Content.Server.Forensics
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{
if (component.DNA == null)
return;
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -308,6 +315,36 @@ namespace Content.Server.Forensics
#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>
/// Transfer DNA from one entity onto the forensics of another
/// </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>
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);
recipientComp.DNAs.Add(donorComp.DNA);

View File

@@ -216,17 +216,11 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
_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 };
RaiseLocalEvent(ent, ref ev);
}
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
{
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
}
// If the entity has the respecive components, then scramble the dna and fingerprint strings
_forensicsSystem.RandomizeDNA(ent);
_forensicsSystem.RandomizeFingerprint(ent);
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
_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);
}
// TODO: Turn this into a relay event.
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
{
// 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.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies;
using Content.Shared.Prying.Components;
@@ -58,8 +58,8 @@ public sealed partial class ZombieSystem
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
/// <summary>
/// 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))
{
//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
_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.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
using Content.Server.Roles;
using Content.Shared.Anomaly.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Cloning;
using Content.Shared.Cloning.Events;
using Content.Shared.Damage;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
@@ -38,7 +40,7 @@ namespace Content.Server.Zombies
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
public const SlotFlags ProtectiveSlots =
SlotFlags.FEET |
@@ -63,6 +65,8 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
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="zombiecomp"></param>
/// <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.
/// </remarks>
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
@@ -292,14 +296,25 @@ namespace Content.Server.Zombies
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
_nameMod.RefreshNameModifiers(target);
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))
args.NameHandled = true;
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
}
// 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]
public sealed partial class CloningPodComponent : Component
{
[ValidatePrototypeId<SinkPortPrototype>]
public const string PodPort = "CloningPodReceiver";
[DataField]
public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
[ViewVariables]
public ContainerSlot BodyContainer = default!;
@@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
/// <summary>
/// The material that is used to clone entities.
/// </summary>
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
/// <summary>
/// The current amount of time it takes to clone a body
/// The current amount of time it takes to clone a body.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float CloningTime = 30f;
/// <summary>
/// The mob to spawn on emag
/// The mob to spawn on emag.
/// </summary>
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntProtoId MobSpawnId = "MobAbomination";
// TODO: Remove this from here when cloning and/or zombies are refactored
[DataField("screamSound")]
/// <summary>
/// The sound played when a mob is spawned from an emagged cloning pod.
/// </summary>
[DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
{
Params = AudioParams.Default.WithVolume(4),
@@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
Gore,
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
{
[DataField("dna"), AutoNetworkedField]
public string DNA = String.Empty;
public string? DNA;
}

View File

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

View File

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

View File

@@ -60,11 +60,13 @@
id: CloningPodSender
name: signal-port-name-pod-receiver
description: signal-port-description-pod-sender
defaultLinks: [ CloningPodReceiver ]
- type: sourcePort
id: MedicalScannerSender
name: signal-port-name-med-scanner-sender
description: signal-port-description-med-scanner-sender
defaultLinks: [ MedicalScannerReceiver ]
- type: sourcePort
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: Thirst
- type: Icon
sprite: Mobs/Species/Slime/parts.rsi # It was like this beforehand, no idea why.
sprite: Mobs/Species/Human/parts.rsi
state: full
- type: Respirator
damage:
@@ -52,7 +52,7 @@
- type: Speech
speechSounds: Bass
- type: HumanoidAppearance
species: Human
species: Dwarf
hideLayersOnEquip:
- Hair
- 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