Cloning Refactor and bugfixes (#35555)
* cloning refactor * cleanup and fixes * don't pick from 0 * give dwarves the correct species * fix dna and bloodstream reagent data cloning * don't copy helmets * be less redundant
This commit is contained in:
parent
c14fdae094
commit
ff5849e487
|
|
@ -1,7 +1,6 @@
|
|||
using Content.Server.Body.Components;
|
||||
using Content.Server.EntityEffects.Effects;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
|
|
@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||
|
||||
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
|
||||
|
||||
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
|
||||
{
|
||||
donorComp.DNA = _forensicsSystem.GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
|
||||
RaiseLocalEvent(entity.Owner, ref ev);
|
||||
}
|
||||
|
||||
// Fill blood solution with BLOOD
|
||||
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
}
|
||||
|
||||
|
|
@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
var bloodData = new List<ReagentData>();
|
||||
var dnaData = new DnaData();
|
||||
|
||||
if (TryComp<DnaComponent>(uid, out var donorComp))
|
||||
{
|
||||
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
|
||||
dnaData.DNA = donorComp.DNA;
|
||||
} else
|
||||
{
|
||||
else
|
||||
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
||||
}
|
||||
|
||||
bloodData.Add(dnaData);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,24 +15,22 @@ 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!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
|
@ -170,8 +167,8 @@ namespace Content.Server.Cloning
|
|||
|
||||
if (mind.UserId.HasValue == false || mind.Session == null)
|
||||
return;
|
||||
// Nyano: Adds scannerComp.MetemKarmaBonus
|
||||
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier, scannerComp.MetemKarmaBonus))
|
||||
// DeltaV: Adds scannerComp.MetemKarmaBonus
|
||||
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier, scannerComp.MetemKarmaBonus))
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,335 @@
|
|||
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.Server.Psionics; // DeltaV
|
||||
using Content.Shared._EE.Silicon.Components; // Goobstation
|
||||
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))));
|
||||
}
|
||||
|
||||
// DeltaV - added karmaBonus
|
||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1, float karmaBonus = 0.25f)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return false;
|
||||
|
||||
// DeltaV - This method should use Entity<CloningPodComponent> pod instead
|
||||
// But I don't want to completely mangle it so we do this here
|
||||
var podEnt = new Entity<CloningPodComponent>(uid, clonePod);
|
||||
|
||||
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;
|
||||
|
||||
if (HasComp<SiliconComponent>(bodyToClone))
|
||||
return false; // Goobstation: Don't clone IPCs.
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
EnsureComp<PotentialPsionicComponent>(mob.Value); // DeltaV
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,382 +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._EE.Silicon.Components; // Goobstation
|
||||
using Content.Server.Psionics; // DeltaV
|
||||
using Content.Server.Traits.Assorted; // DeltaV
|
||||
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 partial class CloningSystem : EntitySystem // DeltaV - Set to partial, see CloningSystem.Metempsychosis.cs
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||
/// </summary>
|
||||
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly EuiManager _euiManager = null!;
|
||||
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
clone = null;
|
||||
if (!_prototype.TryIndex(settingsId, out var settings))
|
||||
return false; // invalid settings
|
||||
|
||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||
public const float EasyModeCloningCost = 0.7f;
|
||||
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
|
||||
return false; // whatever body was to be cloned, was not a humanoid
|
||||
|
||||
public override void Initialize()
|
||||
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
||||
return false; // invalid species
|
||||
|
||||
var attemptEv = new CloningAttemptEvent(settings);
|
||||
RaiseLocalEvent(original, ref attemptEv);
|
||||
if (attemptEv.Cancelled && !settings.ForceCloning)
|
||||
return false; // cannot clone, for example due to the unrevivable trait
|
||||
|
||||
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
|
||||
_humanoidSystem.CloneAppearance(original, clone.Value);
|
||||
|
||||
var componentsToCopy = settings.Components;
|
||||
|
||||
// don't make status effects permanent
|
||||
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
|
||||
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
|
||||
|
||||
foreach (var componentName in componentsToCopy)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
||||
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
||||
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
|
||||
{
|
||||
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
|
||||
_signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
|
||||
}
|
||||
|
||||
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
||||
!EntityManager.EntityExists(entity) ||
|
||||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
|
||||
mindComp.Mind != null)
|
||||
return;
|
||||
|
||||
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
|
||||
_mindSystem.UnVisit(mindId, mind);
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
}
|
||||
|
||||
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
|
||||
{
|
||||
if (clonedComponent.Parent == EntityUid.Invalid ||
|
||||
!EntityManager.EntityExists(clonedComponent.Parent) ||
|
||||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
||||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
||||
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
|
||||
{
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
||||
return;
|
||||
}
|
||||
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
||||
}
|
||||
|
||||
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
|
||||
{
|
||||
pod.ConnectedConsole = null;
|
||||
}
|
||||
|
||||
private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
|
||||
return;
|
||||
|
||||
if (args.Anchored)
|
||||
{
|
||||
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
|
||||
return;
|
||||
}
|
||||
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
|
||||
}
|
||||
// Nyano: Adds float karmaBonus
|
||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1, float karmaBonus = 0.25f)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return false;
|
||||
|
||||
// DeltaV - This method should use Entity<CloningPodComponent> pod instead
|
||||
// But I don't want to completely mangle it so we do this here
|
||||
var podEnt = new Entity<CloningPodComponent>(uid, clonePod);
|
||||
|
||||
if (HasComp<ActiveCloningPodComponent>(uid))
|
||||
return false;
|
||||
|
||||
var mind = mindEnt.Comp;
|
||||
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
||||
{
|
||||
if (EntityManager.EntityExists(clone) &&
|
||||
!_mobStateSystem.IsDead(clone) &&
|
||||
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
||||
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
||||
return false; // Mind already has clone
|
||||
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
||||
return false; // Body controlled by mind is not dead
|
||||
|
||||
// Yes, we still need to track down the client because we need to open the Eui
|
||||
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
|
||||
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
|
||||
return false; // whatever body was to be cloned, was not a humanoid
|
||||
|
||||
if (HasComp<SiliconComponent>(bodyToClone))
|
||||
return false; // Goobstation: Don't clone IPCs.
|
||||
|
||||
// Begin Nyano-code: allow paradox anomalies to be cloned.
|
||||
var pref = humanoid.LastProfileLoaded;
|
||||
|
||||
if (pref == null)
|
||||
return false;
|
||||
// End Nyano-code
|
||||
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);
|
||||
|
||||
// Check if they have the uncloneable trait
|
||||
if (TryComp<UncloneableComponent>(bodyToClone, out _))
|
||||
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-uncloneable-trait-error"),
|
||||
InGameICChatType.Speak, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
|
||||
chance *= failChanceModifier;
|
||||
|
||||
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
||||
|
||||
if (_robustRandom.Prob(chance))
|
||||
{
|
||||
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
||||
clonePod.FailedClone = true;
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// end of genetic damage checks
|
||||
|
||||
// DeltaV - Replaces CloneAppearance with Metem/Clone via FetchAndSpawnMob
|
||||
var mob = FetchAndSpawnMob(podEnt, pref, speciesPrototype, humanoid, bodyToClone, karmaBonus);
|
||||
|
||||
// Nyano - Summary: adds the potential psionic trait to the reanimated mob.
|
||||
EnsureComp<PotentialPsionicComponent>(mob);
|
||||
|
||||
var ev = new CloningEvent(bodyToClone, mob);
|
||||
RaiseLocalEvent(bodyToClone, ref ev);
|
||||
|
||||
if (!ev.NameHandled)
|
||||
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
|
||||
|
||||
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
|
||||
cloneMindReturn.Mind = mind;
|
||||
cloneMindReturn.Parent = uid;
|
||||
_containerSystem.Insert(mob, clonePod.BodyContainer);
|
||||
ClonesWaitingForMind.Add(mind, mob);
|
||||
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
||||
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
||||
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
|
||||
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
|
||||
// Add on special job components to the mob.
|
||||
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
|
||||
{
|
||||
foreach (var special in prototype.Special)
|
||||
{
|
||||
if (special is AddComponentSpecial)
|
||||
special.AfterEquip(mob);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
||||
{
|
||||
cloningPod.Status = status;
|
||||
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
||||
while (query.MoveNext(out var uid, out var _, out var cloning))
|
||||
{
|
||||
if (!_powerReceiverSystem.IsPowered(uid))
|
||||
continue;
|
||||
|
||||
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
||||
continue;
|
||||
|
||||
cloning.CloningProgress += frameTime;
|
||||
if (cloning.CloningProgress < cloning.CloningTime)
|
||||
continue;
|
||||
|
||||
if (cloning.FailedClone)
|
||||
EndFailedCloning(uid, cloning);
|
||||
else
|
||||
Eject(uid, cloning);
|
||||
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
|
||||
RemComp(clone.Value, componentRegistration.Type);
|
||||
CopyComp(original, clone.Value, sourceComp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
||||
/// </summary>
|
||||
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
|
||||
var cloningEv = new CloningEvent(settings, clone.Value);
|
||||
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
|
||||
|
||||
// Add equipment first so that SetEntityName also renames the ID card.
|
||||
if (settings.CopyEquipment != null)
|
||||
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
|
||||
|
||||
var originalName = Name(original);
|
||||
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
|
||||
originalName = nameModComp.BaseName;
|
||||
|
||||
// This will properly set the BaseName and EntityName for the clone.
|
||||
// Adding the component first before renaming will make sure RefreshNameModifers is called.
|
||||
// Without this the name would get reverted to Urist.
|
||||
// If the clone has no name modifiers, NameModifierComponent will be removed again.
|
||||
EnsureComp<NameModifierComponent>(clone.Value);
|
||||
_metaData.SetEntityName(clone.Value, originalName);
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the equipment the original has to the clone.
|
||||
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
|
||||
/// </summary>
|
||||
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
|
||||
return;
|
||||
// Iterate over all inventory slots
|
||||
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
|
||||
while (slotEnumerator.NextItem(out var item, out var slot))
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
// Spawn a copy of the item using the original prototype.
|
||||
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
|
||||
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
|
||||
continue;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return;
|
||||
|
||||
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
||||
return;
|
||||
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
||||
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
||||
clonePod.CloningProgress = 0f;
|
||||
clonePod.UsedBiomass = 0;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
|
||||
{
|
||||
clonePod.FailedClone = false;
|
||||
clonePod.CloningProgress = 0f;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
var transform = Transform(uid);
|
||||
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
||||
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
||||
}
|
||||
|
||||
Solution bloodSolution = new();
|
||||
|
||||
var i = 0;
|
||||
while (i < 1)
|
||||
{
|
||||
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
|
||||
bloodSolution.AddReagent("Blood", 50);
|
||||
if (_robustRandom.Prob(0.2f))
|
||||
i++;
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
||||
|
||||
if (!_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||
}
|
||||
|
||||
clonePod.UsedBiomass = 0;
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
ClonesWaitingForMind.Clear();
|
||||
var prototype = MetaData(item).EntityPrototype;
|
||||
if (prototype != null)
|
||||
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Cloning.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is added to a marker entity in order to spawn a clone of a random player.
|
||||
/// </summary>
|
||||
[RegisterComponent, EntityCategory("Spawner")]
|
||||
public sealed partial class RandomCloneSpawnerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Cloning settings to be used.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using Content.Server.Cloning.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// This deals with spawning and setting up a clone of a random crew member.
|
||||
/// </summary>
|
||||
public sealed class RandomCloneSpawnerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
QueueDel(ent.Owner);
|
||||
|
||||
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
|
||||
{
|
||||
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
|
||||
return;
|
||||
}
|
||||
|
||||
var allHumans = _mind.GetAliveHumans();
|
||||
|
||||
if (allHumans.Count == 0)
|
||||
return;
|
||||
|
||||
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
|
||||
|
||||
if (bodyToClone != null)
|
||||
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -34,8 +35,9 @@ namespace Content.Server.Forensics
|
|||
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
||||
SubscribeLocalEvent<FiberComponent, ContactInteractionEvent>(OnFiberInteract); // DeltaV
|
||||
SubscribeLocalEvent<FiberComponent, MapInitEvent>(OnFiberInit); // DeltaV #1455 - unique glove fibers
|
||||
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);
|
||||
|
|
@ -81,18 +83,20 @@ namespace Content.Server.Forensics
|
|||
|
||||
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||
Dirty(ent);
|
||||
if (ent.Comp.Fingerprint == null)
|
||||
RandomizeFingerprint((ent.Owner, ent.Comp));
|
||||
}
|
||||
|
||||
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
|
||||
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (component.DNA == String.Empty)
|
||||
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
if (ent.Comp.DNA == null)
|
||||
RandomizeDNA((ent.Owner, ent.Comp));
|
||||
else
|
||||
{
|
||||
component.DNA = GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
// If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
|
||||
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +104,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)
|
||||
|
|
@ -119,7 +123,7 @@ namespace Content.Server.Forensics
|
|||
{
|
||||
foreach (EntityUid hitEntity in args.HitEntities)
|
||||
{
|
||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
|
||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
|
||||
component.DNAs.Add(hitEntityComp.DNA);
|
||||
}
|
||||
}
|
||||
|
|
@ -331,6 +335,9 @@ namespace Content.Server.Forensics
|
|||
|
||||
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
||||
{
|
||||
if (component.DNA == null)
|
||||
return;
|
||||
|
||||
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
||||
recipientComp.DNAs.Add(component.DNA);
|
||||
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
||||
|
|
@ -338,6 +345,36 @@ namespace Content.Server.Forensics
|
|||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
|
||||
/// Does nothing if it does not have the DnaComponent.
|
||||
/// </summary>
|
||||
public void RandomizeDNA(Entity<DnaComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.DNA = GenerateDNA();
|
||||
Dirty(ent);
|
||||
|
||||
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Give the entity a new, random fingerprint string.
|
||||
/// Does nothing if it does not have the FingerprintComponent.
|
||||
/// </summary>
|
||||
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer DNA from one entity onto the forensics of another
|
||||
/// </summary>
|
||||
|
|
@ -346,7 +383,7 @@ namespace Content.Server.Forensics
|
|||
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
||||
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
||||
{
|
||||
if (TryComp<DnaComponent>(donor, out var donorComp))
|
||||
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
|
||||
{
|
||||
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
||||
recipientComp.DNAs.Add(donorComp.DNA);
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
|
|||
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Turn this into a relay event.
|
||||
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
// does the user already has this accent?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
|
||||
namespace Content.Server.Traits.Assorted;
|
||||
|
||||
public sealed class UnrevivableSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
|
||||
}
|
||||
|
||||
private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
|
||||
{
|
||||
if (!ent.Comp.Cloneable)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using Content.Server.Actions;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chat;
|
||||
|
|
@ -14,7 +13,7 @@ using Content.Server.NPC.HTN;
|
|||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Abilities.Psionics;
|
||||
using Content.Shared.Abilities.Psionics; // DeltaV
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.Damage;
|
||||
|
|
@ -26,12 +25,11 @@ using Content.Shared.Mobs;
|
|||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.NPC.Components;
|
||||
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;
|
||||
|
|
@ -61,8 +59,8 @@ public sealed partial class ZombieSystem
|
|||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
|
|
@ -235,7 +233,7 @@ public sealed partial class ZombieSystem
|
|||
|
||||
_faction.ClearFactions(target, dirty: false);
|
||||
_faction.AddFaction(target, "Zombie");
|
||||
EnsureComp<NoFriendlyFireComponent>(target); // prevent shitters biting other zombies
|
||||
EnsureComp<NoFriendlyFireComponent>(target); // DeltaV - prevent shitters biting other zombies
|
||||
|
||||
//gives it the funny "Zombie ___" name.
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
|
|
@ -252,7 +250,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"));
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@ using Content.Server.Chat;
|
|||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Emoting.Systems;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -38,7 +40,7 @@ namespace Content.Server.Zombies
|
|||
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
|
||||
public const SlotFlags ProtectiveSlots =
|
||||
SlotFlags.FEET |
|
||||
|
|
@ -63,6 +65,8 @@ namespace Content.Server.Zombies
|
|||
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
||||
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
||||
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
|
||||
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
|
||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
||||
|
|
@ -264,7 +268,7 @@ namespace Content.Server.Zombies
|
|||
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
|
||||
/// <param name="zombiecomp"></param>
|
||||
/// <remarks>
|
||||
/// this currently only restore the name and skin/eye color from before zombified
|
||||
/// this currently only restore the skin/eye color from before zombified
|
||||
/// TODO: completely rethink how zombies are done to allow reversal.
|
||||
/// </remarks>
|
||||
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
|
||||
|
|
@ -284,14 +288,25 @@ namespace Content.Server.Zombies
|
|||
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
|
||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
|
||||
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
|
||||
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
|
||||
{
|
||||
if (UnZombify(args.Source, args.Target, zombiecomp))
|
||||
args.NameHandled = true;
|
||||
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
|
||||
}
|
||||
|
||||
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
|
||||
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
|
||||
{
|
||||
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
|
||||
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
|
||||
}
|
||||
|
||||
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
|
||||
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
|
||||
{
|
||||
_role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a paradox anomaly of a random person when taken by a player.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ParadoxClonerRuleComponent : Component;
|
||||
public sealed partial class ParadoxClonerRuleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> CloningSettings = "BaseClone";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using Content.Server.Antag;
|
||||
using Content.Server.Cloning;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Psionics;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Server.Terminator.Systems;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Mind;
|
||||
|
|
@ -25,11 +27,13 @@ namespace Content.Server.StationEvents.Events;
|
|||
/// </summary>
|
||||
public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly PsionicsSystem _psionics = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
[Dependency] private readonly TerminatorSystem _terminator = default!;
|
||||
|
|
@ -46,30 +50,30 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
|
|||
if (args.Session?.AttachedEntity is not {} spawner)
|
||||
return;
|
||||
|
||||
var settings = _proto.Index(ent.Comp.CloningSettings);
|
||||
Log.Debug($"Rule {ToPrettyString(ent)} creating a paradox anomaly using spawner {spawner}");
|
||||
if (!TrySpawnParadoxAnomaly(spawner, out var clone))
|
||||
if (!TrySpawnParadoxAnomaly(spawner, settings, out var clone))
|
||||
return;
|
||||
|
||||
Log.Info($"Created paradox anomaly {ToPrettyString(clone):clone}");
|
||||
args.Entity = clone;
|
||||
}
|
||||
|
||||
private bool TrySpawnParadoxAnomaly(EntityUid spawner, [NotNullWhen(true)] out EntityUid? clone)
|
||||
private bool TrySpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, [NotNullWhen(true)] out EntityUid? clone)
|
||||
{
|
||||
clone = null;
|
||||
|
||||
// Get a list of potential candidates
|
||||
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)>();
|
||||
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>)>();
|
||||
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
|
||||
while (query.MoveNext(out var uid, out var mindContainer, out var humanoid))
|
||||
{
|
||||
if (humanoid.LastProfileLoaded is {} profile &&
|
||||
mindContainer.Mind is {} mindId &&
|
||||
if (mindContainer.Mind is {} mindId &&
|
||||
!_role.MindIsAntagonist(mindId) &&
|
||||
_role.MindHasRole<JobRoleComponent>(mindId, out var role) &&
|
||||
role?.Comp1.JobPrototype is {} job)
|
||||
{
|
||||
candidates.Add((uid, mindId, job, profile));
|
||||
candidates.Add((uid, mindId, job));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,21 +83,31 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
|
|||
return false;
|
||||
}
|
||||
|
||||
clone = SpawnParadoxAnomaly(spawner, candidates);
|
||||
return true;
|
||||
// tries20 my beloved
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
clone = SpawnParadoxAnomaly(spawner, settings, candidates);
|
||||
if (clone != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Error("Failed to clone any eligible player!");
|
||||
return false;
|
||||
}
|
||||
|
||||
private EntityUid SpawnParadoxAnomaly(EntityUid spawner, List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)> candidates)
|
||||
private EntityUid? SpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, List<(EntityUid, EntityUid, ProtoId<JobPrototype>)> candidates)
|
||||
{
|
||||
// Select a candidate.
|
||||
var (uid, mindId, job, profile) = _random.Pick(candidates);
|
||||
var (uid, mindId, job) = _random.Pick(candidates);
|
||||
|
||||
// Spawn the clone.
|
||||
var coords = Transform(spawner).Coordinates;
|
||||
var coords = _transform.GetMapCoordinates(spawner);
|
||||
var station = _station.GetOwningStation(uid);
|
||||
var spawned = _stationSpawning.SpawnPlayerMob(coords, job, profile, station);
|
||||
if (!_cloning.TryCloning(uid, coords, settings, out var mob))
|
||||
return null;
|
||||
|
||||
// Set the kill target to the chosen player
|
||||
var spawned = mob.Value;
|
||||
_terminator.SetTarget(spawned, mindId);
|
||||
|
||||
// guaranteed psionic power
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace Content.Shared.Cloning.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before a mob is cloned. Cancel to prevent cloning.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a new mob got spawned when cloning a humanoid.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);
|
||||
|
|
@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
|
|||
[RegisterComponent]
|
||||
public sealed partial class CloningPodComponent : Component
|
||||
{
|
||||
[ValidatePrototypeId<SinkPortPrototype>]
|
||||
public const string PodPort = "CloningPodReceiver";
|
||||
[DataField]
|
||||
public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
|
||||
|
||||
[ViewVariables]
|
||||
public ContainerSlot BodyContainer = default!;
|
||||
|
|
@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
|
|||
/// <summary>
|
||||
/// The material that is used to clone entities.
|
||||
/// </summary>
|
||||
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of time it takes to clone a body
|
||||
/// The current amount of time it takes to clone a body.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float CloningTime = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// The mob to spawn on emag
|
||||
/// The mob to spawn on emag.
|
||||
/// </summary>
|
||||
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public EntProtoId MobSpawnId = "MobAbomination";
|
||||
|
||||
// TODO: Remove this from here when cloning and/or zombies are refactored
|
||||
[DataField("screamSound")]
|
||||
/// <summary>
|
||||
/// The sound played when a mob is spawned from an emagged cloning pod.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(4),
|
||||
|
|
@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
|
|||
Gore,
|
||||
NoMind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a new mob got spawned when cloning a humanoid
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public struct CloningEvent
|
||||
{
|
||||
public bool NameHandled = false;
|
||||
|
||||
public readonly EntityUid Source;
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public CloningEvent(EntityUid source, EntityUid target)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
|
||||
|
||||
namespace Content.Shared.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for cloning a humanoid.
|
||||
/// Used to decide which components should be copied.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
|
||||
public string[]? Parents { get; }
|
||||
|
||||
[AbstractDataField]
|
||||
[NeverPushInheritance]
|
||||
public bool Abstract { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if cloning can be prevented by traits etc.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ForceCloning = true;
|
||||
|
||||
/// <summary>
|
||||
/// Which inventory slots will receive a copy of the original's clothing.
|
||||
/// Disabled when null.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for the equipment allowed to be copied.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Blacklist for the equipment allowed to be copied.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
|
||||
/// <summary>
|
||||
/// Components to copy from the original to the clone.
|
||||
/// This only makes a shallow copy of datafields!
|
||||
/// If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AlwaysPushInheritance]
|
||||
public HashSet<string> Components = new();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public record struct TransferDnaEvent()
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// An event to generate and act upon new DNA for an entity.
|
||||
/// Raised on an entity when its DNA has been changed.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GenerateDnaEvent()
|
||||
|
|
|
|||
|
|
@ -158,8 +158,6 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||
if (TryComp<GrammarComponent>(target, out var grammar))
|
||||
grammar.Gender = sourceHumanoid.Gender;
|
||||
|
||||
targetHumanoid.LastProfileLoaded = sourceHumanoid.LastProfileLoaded; // DeltaV - let paradox anomaly be cloned
|
||||
|
||||
Dirty(target, targetHumanoid);
|
||||
}
|
||||
|
||||
|
|
@ -418,8 +416,6 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||
|
||||
humanoid.Age = profile.Age;
|
||||
|
||||
humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned
|
||||
|
||||
RaiseLocalEvent(uid, new ProfileLoadFinishedEvent()); // Shitmed Change
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ public sealed partial class UnrevivableComponent : Component
|
|||
[DataField, AutoNetworkedField]
|
||||
public bool Analyzable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Can this player be cloned using a cloning pod?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Cloneable = false;
|
||||
|
||||
/// <summary>
|
||||
/// The loc string used to provide a reason for being unrevivable
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +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.
|
||||
# DeltaV: change from SOUL IS ABSENT to INCOMPATIBLE GENOME
|
||||
cloning-console-uncloneable-trait-error = ERROR: INCOMPATIBLE GENOME, 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
- type: Thirst
|
||||
- type: Carriable # Carrying system from nyanotrasen.
|
||||
- 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:
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
- type: Speech
|
||||
speechSounds: Bass
|
||||
- type: HumanoidAppearance
|
||||
species: Human
|
||||
species: Dwarf
|
||||
hideLayersOnEquip:
|
||||
- Hair
|
||||
- Snout
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
Reference in New Issue