using System.Linq; using Content.Server.Cloning.Components; using Content.Server.Mind.Components; using Content.Server.Power.Components; using Content.Shared.Audio; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Robust.Shared.Timing; using static Content.Shared.Cloning.SharedCloningPodComponent; namespace Content.Server.Cloning { internal sealed class CloningSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; public readonly Dictionary MindToId = new(); public readonly Dictionary IdToDNA = new(); private int _nextAllocatedMindId = 0; public readonly Dictionary ClonesWaitingForMind = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(Reset); SubscribeLocalEvent(HandleMindAdded); } internal void TransferMindToClone(Mind.Mind mind) { if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) || !EntityManager.EntityExists(entity) || !EntityManager.TryGetComponent(entity, out MindComponent? mindComp) || mindComp.Mind != null) return; mind.TransferTo(entity, ghostCheckOverride: true); mind.UnVisit(); ClonesWaitingForMind.Remove(mind); } private void HandleMindAdded(EntityUid uid, BeingClonedComponent component, MindAddedMessage message) { if (component.Parent == EntityUid.Invalid || !EntityManager.EntityExists(component.Parent) || !EntityManager.TryGetComponent(component.Parent, out var cloningPodComponent) || component.Owner != cloningPodComponent.BodyContainer?.ContainedEntity) { EntityManager.RemoveComponent(component.Owner); return; } cloningPodComponent.UpdateStatus(CloningPodStatus.Cloning); } public override void Update(float frameTime) { // TODO: Make this stateful foreach (var (cloning, power) in EntityManager.EntityQuery()) { if (cloning.UiKnownPowerState != power.Powered) { // Must be *before* update cloning.UiKnownPowerState = power.Powered; UpdateUserInterface(cloning); } if (!power.Powered) continue; if (cloning.BodyContainer.ContainedEntity != null) { cloning.CloningProgress += frameTime; cloning.CloningProgress = MathHelper.Clamp(cloning.CloningProgress, 0f, cloning.CloningTime); } if (cloning.CapturedMind?.Session?.AttachedEntity == cloning.BodyContainer.ContainedEntity) { cloning.Eject(); } } } public void UpdateUserInterface(CloningPodComponent comp) { var idToUser = GetIdToUser(); comp.UserInterface?.SetState( new CloningPodBoundUserInterfaceState( idToUser, // now _timing.CurTime, // progress, time, progressing comp.CloningProgress, comp.CloningTime, // this is duplicate w/ the above check that actually updates progress // better here than on client though comp.UiKnownPowerState && (comp.BodyContainer.ContainedEntity != null), comp.Status == CloningPodStatus.Cloning)); } public void AddToDnaScans(ClonerDNAEntry dna) { if (!MindToId.ContainsKey(dna.Mind)) { int id = _nextAllocatedMindId++; MindToId.Add(dna.Mind, id); IdToDNA.Add(id, dna); } OnChangeMadeToDnaScans(); } public void OnChangeMadeToDnaScans() { foreach (var cloning in EntityManager.EntityQuery()) UpdateUserInterface(cloning); } public bool HasDnaScan(Mind.Mind mind) { return MindToId.ContainsKey(mind); } public Dictionary GetIdToUser() { return IdToDNA.ToDictionary(m => m.Key, m => m.Value.Mind.CharacterName); } public void Reset(RoundRestartCleanupEvent ev) { MindToId.Clear(); IdToDNA.Clear(); ClonesWaitingForMind.Clear(); _nextAllocatedMindId = 0; // We PROBABLY don't need to send out UI interface updates for the dna scan changes during a reset } } // TODO: This needs to be moved to Content.Server.Mobs and made a global point of reference. // For example, GameTicker should be using this, and this should be using ICharacterProfile rather than HumanoidCharacterProfile. // It should carry a reference or copy of itself with the mobs that it affects. // See TODO in MedicalScannerComponent. public struct ClonerDNAEntry { public Mind.Mind Mind; public HumanoidCharacterProfile Profile; public ClonerDNAEntry(Mind.Mind m, HumanoidCharacterProfile hcp) { Mind = m; Profile = hcp; } } }