diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index d04a993226..6dc03fed74 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -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(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"); } /// @@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem var bloodData = new List(); var dnaData = new DnaData(); - if (TryComp(uid, out var donorComp)) - { + if (TryComp(uid, out var donorComp) && donorComp.DNA != null) dnaData.DNA = donorComp.DNA; - } else - { + else dnaData.DNA = Loc.GetString("forensics-dna-unknown"); - } bloodData.Add(dnaData); diff --git a/Content.Server/Cloning/AcceptCloningEui.cs b/Content.Server/Cloning/AcceptCloningEui.cs index 3d4356f8ca..2d1ea93fdb 100644 --- a/Content.Server/Cloning/AcceptCloningEui.cs +++ b/Content.Server/Cloning/AcceptCloningEui.cs @@ -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(); } } diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs index 050e2b7f06..39eac842f0 100644 --- a/Content.Server/Cloning/CloningConsoleSystem.cs +++ b/Content.Server/Cloning/CloningConsoleSystem.cs @@ -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)}."); } diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs new file mode 100644 index 0000000000..594c5ebbb6 --- /dev/null +++ b/Content.Server/Cloning/CloningPodSystem.cs @@ -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 ClonesWaitingForMind = new(); + public readonly ProtoId SettingsId = "CloningPod"; + public const float EasyModeCloningCost = 0.7f; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(Reset); + SubscribeLocalEvent(HandleMindAdded); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnPortDisconnected); + SubscribeLocalEvent(OnAnchor); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnEmagged); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + ent.Comp.BodyContainer = _containerSystem.EnsureContainer(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(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(clonedComponent.Parent, out var cloningPodComponent) || + uid != cloningPodComponent.BodyContainer.ContainedEntity) + { + EntityManager.RemoveComponent(uid); + return; + } + UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent); + } + private void OnPortDisconnected(Entity ent, ref PortDisconnectedEvent args) + { + ent.Comp.ConnectedConsole = null; + } + + private void OnAnchor(Entity ent, ref AnchorStateChangedEvent args) + { + if (ent.Comp.ConnectedConsole == null || !TryComp(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 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 mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1) + { + if (!Resolve(uid, ref clonePod)) + return false; + + if (HasComp(uid)) + return false; + + var mind = mindEnt.Comp; + if (ClonesWaitingForMind.TryGetValue(mind, out var clone)) + { + if (EntityManager.EntityExists(clone) && + !_mobStateSystem.IsDead(clone) && + TryComp(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(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(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(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(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(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(); + 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); + } + } + + /// + /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew. + /// + private void OnEmagged(Entity 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(entity); + _containerSystem.Remove(entity, clonePod.BodyContainer); + clonePod.CloningProgress = 0f; + clonePod.UsedBiomass = 0; + UpdateStatus(uid, CloningPodStatus.Idle, clonePod); + RemCompDeferred(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(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(uid)) + { + _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates); + } + + clonePod.UsedBiomass = 0; + RemCompDeferred(uid); + } + + public void Reset(RoundRestartCleanupEvent ev) + { + ClonesWaitingForMind.Clear(); + } +} diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index d8aac56515..937b311a59 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -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; + +/// +/// System responsible for making a copy of a humanoid's body. +/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead. +/// +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!; + + /// + /// Spawns a clone of the given humanoid mob at the specified location or in nullspace. + /// + public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId 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 ClonesWaitingForMind = new(); - public const float EasyModeCloningCost = 0.7f; + if (!TryComp(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(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(OnComponentInit); - SubscribeLocalEvent(Reset); - SubscribeLocalEvent(HandleMindAdded); - SubscribeLocalEvent(OnPortDisconnected); - SubscribeLocalEvent(OnAnchor); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnEmagged); - } - - private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args) - { - clonePod.BodyContainer = _containerSystem.EnsureContainer(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(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(clonedComponent.Parent, out var cloningPodComponent) || - uid != cloningPodComponent.BodyContainer.ContainedEntity) + if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration)) { - EntityManager.RemoveComponent(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(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 mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1) - { - if (!Resolve(uid, ref clonePod)) - return false; - - if (HasComp(uid)) - return false; - - var mind = mindEnt.Comp; - if (ClonesWaitingForMind.TryGetValue(mind, out var clone)) - { - if (EntityManager.EntityExists(clone) && - !_mobStateSystem.IsDead(clone) && - TryComp(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(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(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(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(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(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(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(); - 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); } } - /// - /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew. - /// - 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(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(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; + } + + /// + /// 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! + /// + public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) + { + if (!TryComp(original, out var originalInventory) || !TryComp(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(entity); - _containerSystem.Remove(entity, clonePod.BodyContainer); - clonePod.CloningProgress = 0f; - clonePod.UsedBiomass = 0; - UpdateStatus(uid, CloningPodStatus.Idle, clonePod); - RemCompDeferred(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(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); } } } diff --git a/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs b/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs new file mode 100644 index 0000000000..ee06a532ef --- /dev/null +++ b/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Cloning; +using Robust.Shared.Prototypes; + +namespace Content.Server.Cloning.Components; + +/// +/// This is added to a marker entity in order to spawn a clone of a random player. +/// +[RegisterComponent, EntityCategory("Spawner")] +public sealed partial class RandomCloneSpawnerComponent : Component +{ + /// + /// Cloning settings to be used. + /// + [DataField] + public ProtoId Settings = "BaseClone"; +} diff --git a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs new file mode 100644 index 0000000000..a645a10890 --- /dev/null +++ b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs @@ -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; + +/// +/// This deals with spawning and setting up a clone of a random crew member. +/// +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(OnMapInit); + } + + private void OnMapInit(Entity 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 _); + } +} diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs index f811bede7b..a52b060391 100644 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs @@ -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(OnInteract); - SubscribeLocalEvent(OnFingerprintInit); - SubscribeLocalEvent(OnDNAInit); + SubscribeLocalEvent(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(OnDNAInit, after: new[] { typeof(BloodstreamSystem) }); SubscribeLocalEvent(OnBeingGibbed); SubscribeLocalEvent(OnMeleeHit); @@ -65,18 +67,20 @@ namespace Content.Server.Forensics private void OnFingerprintInit(Entity 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 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(hitEntity, out var hitEntityComp)) + if (TryComp(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(args.Recipient); recipientComp.DNAs.Add(component.DNA); recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned; @@ -308,6 +315,36 @@ namespace Content.Server.Forensics #region Public API + /// + /// 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. + /// + public void RandomizeDNA(Entity 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); + } + + /// + /// Give the entity a new, random fingerprint string. + /// Does nothing if it does not have the FingerprintComponent. + /// + public void RandomizeFingerprint(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.Fingerprint = GenerateFingerprint(); + Dirty(ent); + } + /// /// Transfer DNA from one entity onto the forensics of another /// @@ -316,7 +353,7 @@ namespace Content.Server.Forensics /// If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) { - if (TryComp(donor, out var donorComp)) + if (TryComp(donor, out var donorComp) && donorComp.DNA != null) { EnsureComp(recipient, out var recipientComp); recipientComp.DNAs.Add(donorComp.DNA); diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index c306e406a1..bd6ffe375c 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -216,18 +216,12 @@ 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(ent, out var dna)) - { - dna.DNA = _forensicsSystem.GenerateDNA(); - var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA }; - RaiseLocalEvent(ent, ref ev); - } - if (TryComp(ent, out var fingerprint)) - { - fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint(); - } - RemComp(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(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); } diff --git a/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs b/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs index 897cd061f4..d55c6e6764 100644 --- a/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs +++ b/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs @@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem SubscribeLocalEvent(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? diff --git a/Content.Server/Traits/Assorted/UnrevivableSystem.cs b/Content.Server/Traits/Assorted/UnrevivableSystem.cs new file mode 100644 index 0000000000..c2c8ee9a50 --- /dev/null +++ b/Content.Server/Traits/Assorted/UnrevivableSystem.cs @@ -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(OnCloningAttempt); + } + + private void OnCloningAttempt(Entity ent, ref CloningAttemptEvent args) + { + if (!ent.Comp.Cloneable) + args.Cancelled = true; + } +} diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 82c9e2dacc..b393850497 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -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!; /// /// 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")); diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index ec50e11a0c..aa5c2682bc 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -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(OnZombieCloning); SubscribeLocalEvent(OnSleepAttempt); SubscribeLocalEvent(OnGetCharacterDeadIC); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindRemoved); SubscribeLocalEvent(OnPendingMapInit); SubscribeLocalEvent(OnBeforeRemoveAnomalyOnDeath); @@ -272,7 +276,7 @@ namespace Content.Server.Zombies /// the entity you want to unzombify (different from source in case of cloning, for example) /// /// - /// 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. /// 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 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 ent, ref MindAddedMessage args) + { + if (!_role.MindHasRole(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 ent, ref MindRemovedMessage args) + { + _role.MindTryRemoveRole(args.Mind); } } } diff --git a/Content.Shared/Cloning/CloningEvents.cs b/Content.Shared/Cloning/CloningEvents.cs new file mode 100644 index 0000000000..bd6645404c --- /dev/null +++ b/Content.Shared/Cloning/CloningEvents.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Cloning.Events; + +/// +/// Raised before a mob is cloned. Cancel to prevent cloning. +/// +[ByRefEvent] +public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false); + +/// +/// Raised after a new mob got spawned when cloning a humanoid. +/// +[ByRefEvent] +public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid); diff --git a/Content.Shared/Cloning/CloningPodComponent.cs b/Content.Shared/Cloning/CloningPodComponent.cs index d588b62eb3..17f733c8f3 100644 --- a/Content.Shared/Cloning/CloningPodComponent.cs +++ b/Content.Shared/Cloning/CloningPodComponent.cs @@ -10,8 +10,8 @@ namespace Content.Shared.Cloning; [RegisterComponent] public sealed partial class CloningPodComponent : Component { - [ValidatePrototypeId] - public const string PodPort = "CloningPodReceiver"; + [DataField] + public ProtoId PodPort = "CloningPodReceiver"; [ViewVariables] public ContainerSlot BodyContainer = default!; @@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component /// /// The material that is used to clone entities. /// - [DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public ProtoId RequiredMaterial = "Biomass"; /// - /// The current amount of time it takes to clone a body + /// The current amount of time it takes to clone a body. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float CloningTime = 30f; /// - /// The mob to spawn on emag + /// The mob to spawn on emag. /// - [DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntProtoId MobSpawnId = "MobAbomination"; - // TODO: Remove this from here when cloning and/or zombies are refactored - [DataField("screamSound")] + /// + /// The sound played when a mob is spawned from an emagged cloning pod. + /// + [DataField] public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams") { Params = AudioParams.Default.WithVolume(4), @@ -74,21 +76,3 @@ public enum CloningPodStatus : byte Gore, NoMind } - -/// -/// Raised after a new mob got spawned when cloning a humanoid -/// -[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; - } -} diff --git a/Content.Shared/Cloning/CloningSettingsPrototype.cs b/Content.Shared/Cloning/CloningSettingsPrototype.cs new file mode 100644 index 0000000000..3828e6c0cf --- /dev/null +++ b/Content.Shared/Cloning/CloningSettingsPrototype.cs @@ -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; + +/// +/// Settings for cloning a humanoid. +/// Used to decide which components should be copied. +/// +[Prototype] +public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; + + [ParentDataField(typeof(PrototypeIdArraySerializer))] + public string[]? Parents { get; } + + [AbstractDataField] + [NeverPushInheritance] + public bool Abstract { get; } + + /// + /// Determines if cloning can be prevented by traits etc. + /// + [DataField] + public bool ForceCloning = true; + + /// + /// Which inventory slots will receive a copy of the original's clothing. + /// Disabled when null. + /// + [DataField] + public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET; + + /// + /// Whitelist for the equipment allowed to be copied. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist for the equipment allowed to be copied. + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709 + /// + /// 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! + /// + [DataField] + [AlwaysPushInheritance] + public HashSet Components = new(); +} diff --git a/Content.Shared/Forensics/Components/DnaComponent.cs b/Content.Shared/Forensics/Components/DnaComponent.cs index 0dfa92146b..a9d2f7ea8b 100644 --- a/Content.Shared/Forensics/Components/DnaComponent.cs +++ b/Content.Shared/Forensics/Components/DnaComponent.cs @@ -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; } diff --git a/Content.Shared/Forensics/Events.cs b/Content.Shared/Forensics/Events.cs index c346d08536..0506f48a3d 100644 --- a/Content.Shared/Forensics/Events.cs +++ b/Content.Shared/Forensics/Events.cs @@ -53,7 +53,7 @@ public record struct TransferDnaEvent() } /// -/// An event to generate and act upon new DNA for an entity. +/// Raised on an entity when its DNA has been changed. /// [ByRefEvent] public record struct GenerateDnaEvent() diff --git a/Content.Shared/Traits/Assorted/UnrevivableComponent.cs b/Content.Shared/Traits/Assorted/UnrevivableComponent.cs index 19b4cac5e0..44af27f5ea 100644 --- a/Content.Shared/Traits/Assorted/UnrevivableComponent.cs +++ b/Content.Shared/Traits/Assorted/UnrevivableComponent.cs @@ -14,6 +14,12 @@ public sealed partial class UnrevivableComponent : Component [DataField, AutoNetworkedField] public bool Analyzable = true; + /// + /// Can this player be cloned using a cloning pod? + /// + [DataField, AutoNetworkedField] + public bool Cloneable = false; + /// /// The loc string used to provide a reason for being unrevivable /// diff --git a/Resources/Locale/en-US/medical/components/cloning-console-component.ftl b/Resources/Locale/en-US/medical/components/cloning-console-component.ftl index b801f92580..c01cc8b5c6 100644 --- a/Resources/Locale/en-US/medical/components/cloning-console-component.ftl +++ b/Resources/Locale/en-US/medical/components/cloning-console-component.ftl @@ -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. diff --git a/Resources/Prototypes/DeviceLinking/source_ports.yml b/Resources/Prototypes/DeviceLinking/source_ports.yml index 5c32734726..adbf4df134 100644 --- a/Resources/Prototypes/DeviceLinking/source_ports.yml +++ b/Resources/Prototypes/DeviceLinking/source_ports.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml new file mode 100644 index 0000000000..64ae4f9ad8 --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 6ce9c80a25..e3f0e5a5c1 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -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 diff --git a/Resources/Textures/Markers/paradox_clone.rsi/meta.json b/Resources/Textures/Markers/paradox_clone.rsi/meta.json new file mode 100644 index 0000000000..e586347031 --- /dev/null +++ b/Resources/Textures/Markers/paradox_clone.rsi/meta.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Markers/paradox_clone.rsi/preview.png b/Resources/Textures/Markers/paradox_clone.rsi/preview.png new file mode 100644 index 0000000000..8b83969954 Binary files /dev/null and b/Resources/Textures/Markers/paradox_clone.rsi/preview.png differ