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;
[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()
{
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);
}
reagentData.AddRange(GetEntityBloodData(entity.Owner));
}
}
+ else
+ Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
/// <summary>
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);
{
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)
return;
}
- _cloningSystem.TransferMindToClone(_mindId, _mind);
+ _cloningPodSystem.TransferMindToClone(_mindId, _mind);
Close();
}
}
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;
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!;
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)}.");
}
--- /dev/null
+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();
+ }
+}
-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()
- {
- base.Initialize();
+ if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
+ return false; // invalid species
- 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);
- }
+ 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
- private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
- {
- clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
- _signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
- }
+ clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
+ _humanoidSystem.CloneAppearance(original, clone.Value);
- 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;
+ var componentsToCopy = settings.Components;
- _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(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);
- }
+ // 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)!);
- private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
+ foreach (var componentName in componentsToCopy)
{
- 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))
- 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)
- _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))
+ if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
{
- 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;
- }
+ Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
+ continue;
}
- // 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))
+ if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
- foreach (var special in prototype.Special)
- {
- if (special is AddComponentSpecial)
- special.AfterEquip(mob);
- }
+ if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
+ RemComp(clone.Value, componentRegistration.Type);
+ CopyComp(original, clone.Value, sourceComp);
}
-
- 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;
+ var cloningEv = new CloningEvent(settings, clone.Value);
+ RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
- 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(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
- {
- if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
- return;
+ // 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);
- if (_emag.CheckFlag(uid, EmagType.Interaction))
- return;
+ var originalName = Name(original);
+ if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
+ originalName = nameModComp.BaseName;
- if (!this.IsPowered(uid, EntityManager))
- return;
+ // 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);
- _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);
- }
+ _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
+ return true;
+ }
- private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
+ /// <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))
{
- 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 _);
+ // 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))
- {
- _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
- }
+ if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
+ continue;
- 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);
}
}
}
--- /dev/null
+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";
+}
--- /dev/null
+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 _);
+ }
+}
using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
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);
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);
}
}
{
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)
{
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);
}
}
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;
#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>
/// <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);
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();
- }
- RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
+ // 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);
}
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?
--- /dev/null
+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;
+ }
+}
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;
[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
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"));
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;
[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 |
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);
/// <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)
_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);
}
}
}
--- /dev/null
+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);
[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!;
/// <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),
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;
- }
-}
--- /dev/null
+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();
+}
public sealed partial class DnaComponent : Component
{
[DataField("dna"), AutoNetworkedField]
- public string DNA = String.Empty;
+ public string? DNA;
}
}
/// <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()
[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>
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.
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
--- /dev/null
+# 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
- 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:
- type: Speech
speechSounds: Bass
- type: HumanoidAppearance
- species: Human
+ species: Dwarf
hideLayersOnEquip:
- Hair
- Snout
--- /dev/null
+{
+ "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"
+ }
+ ]
+}
\ No newline at end of file