From e40e3fa267d64a34ee6cca9a7cd0069e393014ea Mon Sep 17 00:00:00 2001 From: SoulSloth <67545203+SoulSloth@users.noreply.github.com> Date: Wed, 2 Sep 2020 06:07:54 -0400 Subject: [PATCH] Cloning (#1932) * Add art assets for cloning * Added a 'Scan DNA' button to the medical scanner * Made the UI update unconditional for the medical scanner until checks for power changes are in place * Update Medical scanner to reflect powered status and fix #1774 * added a 'scan dna' button the the medical scanner that will add the contained bodies Uid to a list in CloningSystem, fixed an issue with the menu not populating if the scanner starts in an unpowered state * Add disabling logic to 'Scan DNA' button on medical scanner * Removed un-used libraries * changed scan dna button to Scan and Save DNA * Added cloning machine code infrastructure copied from Medical Scanner * Added a list to cloning menu containing some numbers * Cloning Machine UI sends a message to the cloning component with the entityUID * New scans now show up in cloning pod menu * fixed cloning machine collision shape * cloning machine can now spawn the right player profile assuming the attatched entity is still correct. * refactored cloning system to use a map of integer ids to player Minds * Added a return to body cloning loop for the ghost * Fixed warning for _playerManager being possibly null, added TODO note for ghost return to body button acting as a toggle * removed #nullable from cloningMachineWindow" * Trying to get rid of nullable error * fix CloningMachine to not initilize with it's owner components * updated CloningMachine server component to play nice with the new nullable rules * replace flag with eventBus message for sending a ghosts mind to a clone body * Refactor cloning so that a popup option is used to get user consent for cloning * Refactoring * Reverting unused changes for cloning component * Added proper cloning pod sprites and a visualizer so 'idle' and 'cloning' states are properly reflected * added missing robust toolbox contents * Added cloning NoMind State and made cloning take time * Added cloning progress bar and mind status indicator to cloning pod * Added missing localization calls, removeed 'returned to cloned body' from ghostUI * Added unsubscribe for cloningStartedMessage in Mindcomponent.cs OnRemove * Added eject button to cloningMachine and clamped the cloning progress bar to 100% * Added condition to eject body on cloningmachine so bodies can't be ejected until cloning is done * Add click-dragOn functionality to the medical scanner for things with a bodyManager * Messed with scan query so it doesn't fail on dead bodies as long as Mind still owns the mob * refactored clonning scan check on medical scanner so it doesn't do a linq query * merge with rogue toolbox * Change the name of Cloning Machine to the less generic Cloning Pod * Changed Cloning Pod so it pauses cloning while the power is out * Removed the evil LocalizationManager from the cloning menus and used the static Loc instead * removed localization dependency from bound accpetCloning user interface * Removed Ilocalization dependency I accidentally added to ghost ui * Update Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs Co-authored-by: Exp * Changed null check to tryget in case for cloning UiButton.Clone * Parameterized Cloning time on serverside component * tried to reset Robust toolbox module to current master * Added null check to ghost client component message handling, unsubscribe to the mind component listening to the cloning question ui, fixed _clonningProgress typo, moved CloningPod component dependencies to actually be dependencies, removed un-needed disposals of cloning windows, added disposals missing in boundUserInterfaces. * Reset submodule * corrected exception for cloning pod visualizer to refer to cloning pod state and not medical scanner state * Fix typo * Unsubscribe from onUiReceiveMessage in mindcomponent in the onRemove function, not the acceptcloningui function * unsubscribe from OnUiReceiveMessage in CloningPodComponent * unssubscribe from ghostreturn message in cloningpodComponent onRemove Co-authored-by: Exp Co-authored-by: DrSmugleaf --- .../AcceptCloningBoundUserInterface.cs | 45 ++ .../Components/AcceptCloningWindow.cs | 50 ++ .../Components/Actor/CharacterInterface.cs | 1 + .../Components/Body/BodyManagerComponent.cs | 10 +- .../CloningPodBoundUserInterface.cs | 60 +++ .../CloningPod/CloningPodVisualizer.cs | 41 ++ .../Components/CloningPod/CloningPodWindow.cs | 442 ++++++++++++++++++ .../MedicalScanner/MedicalScannerComponent.cs | 13 + .../MedicalScanner/MedicalScannerWindow.cs | 2 +- .../Components/Observer/GhostComponent.cs | 7 +- Content.Client/IgnoredComponents.cs | 2 +- Content.Client/UserInterface/GhostGui.cs | 4 +- .../Components/Medical/CloningPodComponent.cs | 235 ++++++++++ .../Medical/MedicalScannerComponent.cs | 57 ++- .../Components/Mobs/MindComponent.cs | 59 ++- .../Components/Observer/GhostComponent.cs | 32 +- .../EntitySystems/CloningSystem.cs | 32 +- .../Medical/SharedCloningPodComponent.cs | 72 +++ .../Observer/SharedGhostComponent.cs | 7 + .../SharedAcceptCloningComponent.cs | 36 ++ .../Constructible/Power/cloning_machine.yml | 44 ++ .../Entities/Mobs/Species/human.yml | 6 +- .../meta.json | 0 .../pod_0.png | Bin .../scanner.png | Bin .../scanner_maintenance.png | Bin .../scanner_occupied.png | Bin .../scanner_open.png | Bin .../scanner_open_maintenance.png | Bin .../scanner_open_unpowered.png | Bin .../scanner_unpowered.png | Bin .../Specific/Medical/cloning.rsi/meta.json | 56 +++ .../Specific/Medical/cloning.rsi/pod_0.png | Bin 0 -> 1319 bytes .../Specific/Medical/cloning.rsi/pod_1.png | Bin 0 -> 2967 bytes .../Specific/Medical/cloning.rsi/pod_e.png | Bin 0 -> 2937 bytes .../Specific/Medical/cloning.rsi/pod_g.png | Bin 0 -> 3907 bytes 36 files changed, 1276 insertions(+), 37 deletions(-) create mode 100644 Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/AcceptCloningWindow.cs create mode 100644 Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs create mode 100644 Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs create mode 100644 Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs create mode 100644 Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs create mode 100644 Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs create mode 100644 Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/meta.json (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/pod_0.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_maintenance.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_occupied.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_open.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_open_maintenance.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_open_unpowered.png (100%) rename Resources/Textures/Objects/Specific/Medical/{Cloning.rsi => BodyScanner.rsi}/scanner_unpowered.png (100%) create mode 100644 Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json create mode 100644 Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png create mode 100644 Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png create mode 100644 Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png create mode 100644 Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png diff --git a/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs b/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs new file mode 100644 index 0000000000..9b38059110 --- /dev/null +++ b/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs @@ -0,0 +1,45 @@ +using Content.Shared.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; + +namespace Content.Client.GameObjects.Components +{ + [UsedImplicitly] + public class AcceptCloningBoundUserInterface : BoundUserInterface + { + + public AcceptCloningBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private AcceptCloningWindow _window; + + protected override void Open() + { + base.Open(); + + _window = new AcceptCloningWindow(); + _window.OnClose += Close; + _window.DenyButton.OnPressed += _ => _window.Close(); + _window.ConfirmButton.OnPressed += _ => + { + SendMessage( + new SharedAcceptCloningComponent.UiButtonPressedMessage( + SharedAcceptCloningComponent.UiButton.Accept)); + _window.Close(); + }; + _window.OpenCentered(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + + } +} diff --git a/Content.Client/GameObjects/Components/AcceptCloningWindow.cs b/Content.Client/GameObjects/Components/AcceptCloningWindow.cs new file mode 100644 index 0000000000..1731cc9676 --- /dev/null +++ b/Content.Client/GameObjects/Components/AcceptCloningWindow.cs @@ -0,0 +1,50 @@ +#nullable enable +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; + +namespace Content.Client.GameObjects.Components +{ + public sealed class AcceptCloningWindow : SS14Window + { + public readonly Button DenyButton; + public readonly Button ConfirmButton; + + public AcceptCloningWindow() + { + + Title = Loc.GetString("Cloning Machine"); + + Contents.AddChild(new VBoxContainer + { + Children = + { + new VBoxContainer + { + Children = + { + (new Label + { + Text = Loc.GetString("You are being cloned! Transfer your soul to the clone body?") + }), + new HBoxContainer + { + Children = + { + (ConfirmButton = new Button + { + Text = Loc.GetString("Yes"), + }), + (DenyButton = new Button + { + Text = Loc.GetString("No"), + }) + } + }, + } + }, + } + }); + } + } +} diff --git a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs index f776411554..0e6e534971 100644 --- a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs +++ b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Client.GameObjects.Components.Mobs; using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Interfaces.Input; diff --git a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs index fc54c88a78..83df6704b7 100644 --- a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs +++ b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs @@ -1,8 +1,10 @@ #nullable enable using Content.Client.GameObjects.Components.Disposal; +using Content.Client.GameObjects.Components.MedicalScanner; using Content.Client.Interfaces.GameObjects.Components.Interaction; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Medical; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; @@ -21,7 +23,13 @@ namespace Content.Client.GameObjects.Components.Body public bool ClientCanDropOn(CanDropEventArgs eventArgs) { - return eventArgs.Target.HasComponent(); + if ( + eventArgs.Target.HasComponent()|| + eventArgs.Target.HasComponent()) + { + return true; + } + return false; } public bool ClientCanDrag(CanDragEventArgs eventArgs) diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs new file mode 100644 index 0000000000..63760a880e --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Medical; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + [UsedImplicitly] + public class CloningPodBoundUserInterface : BoundUserInterface + { + public CloningPodBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private CloningPodWindow _window; + + protected override void Open() + { + base.Open(); + + + _window = new CloningPodWindow(new Dictionary()); + _window.OnClose += Close; + _window.CloneButton.OnPressed += _ => + { + if (_window.SelectedScan != null) + { + SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Clone, (int) _window.SelectedScan)); + } + }; + _window.EjectButton.OnPressed += _ => + { + SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Eject, null)); + }; + _window.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + _window.Populate((CloningPodBoundUserInterfaceState) state); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs new file mode 100644 index 0000000000..6838794e8c --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs @@ -0,0 +1,41 @@ +using System; +using Content.Shared.GameObjects.Components.Medical; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent.CloningPodStatus; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + public class CloningPodVisualizer : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + var sprite = component.Owner.GetComponent(); + if (!component.TryGetData(CloningPodVisuals.Status, out CloningPodStatus status)) return; + sprite.LayerSetState(CloningPodVisualLayers.Machine, StatusToMachineStateId(status)); + } + + private string StatusToMachineStateId(CloningPodStatus status) + { + //TODO: implement NoMind for if the mind is not yet in the body + //TODO: Find a use for GORE POD + switch (status) + { + case Cloning: return "pod_1"; + case NoMind: return "pod_e"; + case Gore: return "pod_g"; + case Idle: return "pod_0"; + default: + throw new ArgumentOutOfRangeException(nameof(status), status, "unknown CloningPodStatus"); + } + } + + public enum CloningPodVisualLayers + { + Machine, + } + } +} diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs new file mode 100644 index 0000000000..8cc1c3b909 --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs @@ -0,0 +1,442 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + public sealed class CloningPodWindow : SS14Window + { + private Dictionary _scanManager; + + private readonly VBoxContainer _mainVBox; + private readonly ScanListContainer _scanList; + private readonly LineEdit _searchBar; + private readonly Button _clearButton; + public readonly Button CloneButton; + public readonly Button EjectButton; + private readonly CloningScanButton _measureButton; + private CloningScanButton? _selectedButton; + private Label _progressLabel; + private readonly ProgressBar _cloningProgressBar; + private Label _mindState; + + protected override Vector2 ContentsMinimumSize => _mainVBox?.CombinedMinimumSize ?? Vector2.Zero; + private CloningPodBoundUserInterfaceState _lastUpdate = null!; + + // List of scans that are visible based on current filter criteria. + private readonly Dictionary _filteredScans = new Dictionary(); + + // The indices of the visible scans last time UpdateVisibleScans was ran. + // This is inclusive, so end is the index of the last scan, not right after it. + private (int start, int end) _lastScanIndices; + + public int? SelectedScan; + + protected override Vector2? CustomSize => (250, 300); + + public CloningPodWindow( + Dictionary scanManager) + { + _scanManager = scanManager; + + + Title = Loc.GetString("Cloning Machine"); + + Contents.AddChild(_mainVBox = new VBoxContainer + { + Children = + { + new HBoxContainer + { + Children = + { + (_searchBar = new LineEdit + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + PlaceHolder = Loc.GetString("Search") + }), + + (_clearButton = new Button + { + Disabled = true, + Text = Loc.GetString("Clear"), + }) + } + }, + new ScrollContainer + { + CustomMinimumSize = new Vector2(200.0f, 0.0f), + SizeFlagsVertical = SizeFlags.FillExpand, + Children = + { + (_scanList = new ScanListContainer()) + } + }, + new VBoxContainer + { + Children = + { + (CloneButton = new Button + { + Text = Loc.GetString("Clone") + }) + } + }, + (_measureButton = new CloningScanButton {Visible = false}), + (_cloningProgressBar = new ProgressBar + { + CustomMinimumSize = (200, 20), + SizeFlagsHorizontal = SizeFlags.Fill, + MinValue = 0, + MaxValue = 10, + Page = 0, + Value = 0.5f, + Children = + { + (_progressLabel = new Label()) + } + }), + (EjectButton = new Button + { + Text = Loc.GetString("Eject Body") + }), + new HBoxContainer + { + Children = + { + new Label() + { + Text = Loc.GetString("Neural Interface: ") + }, + (_mindState = new Label() + { + Text = Loc.GetString("No Activity"), + FontColorOverride = Color.Red + }), + } + } + } + }); + + + _searchBar.OnTextChanged += OnSearchBarTextChanged; + _clearButton.OnPressed += OnClearButtonPressed; + + BuildEntityList(); + + _searchBar.GrabKeyboardFocus(); + } + + public void Populate(CloningPodBoundUserInterfaceState state) + { + //Ignore useless updates or we can't interact with the UI + //TODO: come up with a better comparision, probably write a comparator because '.Equals' doesn't work + if (_lastUpdate == null || _lastUpdate.MindIdName.Count != state.MindIdName.Count) + { + _scanManager = state.MindIdName; + BuildEntityList(); + _lastUpdate = state; + } + + var percentage = state.Progress / _cloningProgressBar.MaxValue * 100; + _progressLabel.Text = $"{percentage:0}%"; + + _cloningProgressBar.Value = state.Progress; + _mindState.Text = Loc.GetString(state.MindPresent ? "Consciousness Detected" : "No Activity"); + _mindState.FontColorOverride = state.MindPresent ? Color.Green : Color.Red; + } + + private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) + { + BuildEntityList(args.Text); + _clearButton.Disabled = string.IsNullOrEmpty(args.Text); + } + + private void OnClearButtonPressed(BaseButton.ButtonEventArgs args) + { + _searchBar.Clear(); + BuildEntityList(""); + } + + + private void BuildEntityList(string? searchStr = null) + { + _filteredScans.Clear(); + _scanList.RemoveAllChildren(); + // Reset last scan indices so it automatically updates the entire list. + _lastScanIndices = (0, -1); + _scanList.RemoveAllChildren(); + _selectedButton = null; + searchStr = searchStr?.ToLowerInvariant(); + + foreach (var scan in _scanManager) + { + if (searchStr != null && !_doesScanMatchSearch(scan.Value, searchStr)) + { + continue; + } + + _filteredScans.Add(scan.Key, scan.Value); + } + + //TODO: set up sort + //_filteredScans.Sort((a, b) => string.Compare(a.ToString(), b.ToString(), StringComparison.Ordinal)); + + _scanList.TotalItemCount = _filteredScans.Count; + } + + private void UpdateVisibleScans() + { + // Update visible buttons in the scan list. + + // Calculate index of first scan to render based on current scroll. + var height = _measureButton.CombinedMinimumSize.Y + ScanListContainer.Separation; + var offset = -_scanList.Position.Y; + var startIndex = (int) Math.Floor(offset / height); + _scanList.ItemOffset = startIndex; + + var (prevStart, prevEnd) = _lastScanIndices; + + // Calculate index of final one. + var endIndex = startIndex - 1; + var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button. + + while (spaceUsed < _scanList.Parent!.Height) + { + spaceUsed += height; + endIndex += 1; + } + + endIndex = Math.Min(endIndex, _filteredScans.Count - 1); + + if (endIndex == prevEnd && startIndex == prevStart) + { + // Nothing changed so bye. + return; + } + + _lastScanIndices = (startIndex, endIndex); + + // Delete buttons at the start of the list that are no longer visible (scrolling down). + for (var i = prevStart; i < startIndex && i <= prevEnd; i++) + { + var control = (CloningScanButton) _scanList.GetChild(0); + DebugTools.Assert(control.Index == i); + _scanList.RemoveChild(control); + } + + // Delete buttons at the end of the list that are no longer visible (scrolling up). + for (var i = prevEnd; i > endIndex && i >= prevStart; i--) + { + var control = (CloningScanButton) _scanList.GetChild(_scanList.ChildCount - 1); + DebugTools.Assert(control.Index == i); + _scanList.RemoveChild(control); + } + + var array = _filteredScans.ToArray(); + + // Create buttons at the start of the list that are now visible (scrolling up). + for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--) + { + InsertEntityButton(array[i], true, i); + } + + // Create buttons at the end of the list that are now visible (scrolling down). + for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++) + { + InsertEntityButton(array[i], false, i); + } + } + + // Create a spawn button and insert it into the start or end of the list. + private void InsertEntityButton(KeyValuePair scan, bool insertFirst, int index) + { + var button = new CloningScanButton + { + Scan = scan.Value, + Id = scan.Key, + Index = index // We track this index purely for debugging. + }; + button.ActualButton.OnToggled += OnItemButtonToggled; + var entityLabelText = scan.Value; + + button.EntityLabel.Text = entityLabelText; + + if (scan.Key == SelectedScan) + { + _selectedButton = button; + _selectedButton.ActualButton.Pressed = true; + } + + //TODO: replace with body's face + /*var tex = IconComponent.GetScanIcon(scan, resourceCache); + var rect = button.EntityTextureRect; + if (tex != null) + { + rect.Texture = tex.Default; + } + else + { + rect.Dispose(); + } + + rect.Dispose(); + */ + + _scanList.AddChild(button); + if (insertFirst) + { + button.SetPositionInParent(0); + } + } + + private static bool _doesScanMatchSearch(string scan, string searchStr) + { + return scan.ToLowerInvariant().Contains(searchStr); + } + + private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args) + { + var item = (CloningScanButton) args.Button.Parent!; + if (_selectedButton == item) + { + _selectedButton = null; + SelectedScan = null; + return; + } + else if (_selectedButton != null) + { + _selectedButton.ActualButton.Pressed = false; + } + + _selectedButton = null; + SelectedScan = null; + + _selectedButton = item; + SelectedScan = item.Id; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + UpdateVisibleScans(); + } + + private class ScanListContainer : Container + { + // Quick and dirty container to do virtualization of the list. + // Basically, get total item count and offset to put the current buttons at. + // Get a constant minimum height and move the buttons in the list up to match the scrollbar. + private int _totalItemCount; + private int _itemOffset; + + public int TotalItemCount + { + get => _totalItemCount; + set + { + _totalItemCount = value; + MinimumSizeChanged(); + } + } + + public int ItemOffset + { + get => _itemOffset; + set + { + _itemOffset = value; + UpdateLayout(); + } + } + + public const float Separation = 2; + + protected override Vector2 CalculateMinimumSize() + { + if (ChildCount == 0) + { + return Vector2.Zero; + } + + var first = GetChild(0); + + var (minX, minY) = first.CombinedMinimumSize; + + return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation); + } + + protected override void LayoutUpdateOverride() + { + if (ChildCount == 0) + { + return; + } + + var first = GetChild(0); + + var height = first.CombinedMinimumSize.Y; + var offset = ItemOffset * height + (ItemOffset - 1) * Separation; + + foreach (var child in Children) + { + FitChildInBox(child, UIBox2.FromDimensions(0, offset, Width, height)); + offset += Separation + height; + } + } + } + + [DebuggerDisplay("cloningbutton {" + nameof(Index) + "}")] + private class CloningScanButton : Control + { + public string Scan { get; set; } = default!; + public int Id { get; set; } + public Button ActualButton { get; private set; } + public Label EntityLabel { get; private set; } + public TextureRect EntityTextureRect { get; private set; } + public int Index { get; set; } + + public CloningScanButton() + { + AddChild(ActualButton = new Button + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + ToggleMode = true, + }); + + AddChild(new HBoxContainer + { + Children = + { + (EntityTextureRect = new TextureRect + { + CustomMinimumSize = (32, 32), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter, + SizeFlagsVertical = SizeFlags.ShrinkCenter, + Stretch = TextureRect.StretchMode.KeepAspectCentered, + CanShrink = true + }), + (EntityLabel = new Label + { + SizeFlagsVertical = SizeFlags.ShrinkCenter, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Text = "", + ClipText = true + }) + } + }); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs new file mode 100644 index 0000000000..62bae7d387 --- /dev/null +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.GameObjects.Components.Medical; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.MedicalScanner +{ + + [RegisterComponent] + [ComponentReference(typeof(SharedMedicalScannerComponent))] + public class MedicalScannerComponent : SharedMedicalScannerComponent + { + + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs index 4668854fbf..59edf052f3 100644 --- a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs @@ -24,7 +24,7 @@ namespace Content.Client.GameObjects.Components.MedicalScanner { (ScanButton = new Button { - Text = "Scan and Save DNA" + Text = Loc.GetString("Scan and Save DNA") }), (_diagnostics = new Label { diff --git a/Content.Client/GameObjects/Components/Observer/GhostComponent.cs b/Content.Client/GameObjects/Components/Observer/GhostComponent.cs index 077eae387c..f25c709136 100644 --- a/Content.Client/GameObjects/Components/Observer/GhostComponent.cs +++ b/Content.Client/GameObjects/Components/Observer/GhostComponent.cs @@ -18,8 +18,7 @@ namespace Content.Client.GameObjects.Components.Observer private GhostGui _gui; - [ViewVariables(VVAccess.ReadOnly)] - public bool CanReturnToBody { get; private set; } = true; + [ViewVariables(VVAccess.ReadOnly)] public bool CanReturnToBody { get; private set; } = true; private bool _isAttached; @@ -51,7 +50,8 @@ namespace Content.Client.GameObjects.Components.Observer base.Initialize(); if (Owner.TryGetComponent(out SpriteComponent component)) - component.Visible = _playerManager.LocalPlayer.ControlledEntity?.HasComponent() ?? false; + component.Visible = + _playerManager.LocalPlayer.ControlledEntity?.HasComponent() ?? false; } public override void HandleMessage(ComponentMessage message, IComponent component) @@ -98,7 +98,6 @@ namespace Content.Client.GameObjects.Components.Observer { _gui?.Update(); } - } } } diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 43269da4ad..af320568fa 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -9,6 +9,7 @@ "Breakable", "Pickaxe", "Interactable", + "CloningPod", "Destructible", "Temperature", "Explosive", @@ -58,7 +59,6 @@ "AccessReader", "IdCardConsole", "Airlock", - "MedicalScanner", "WirePlacer", "Drink", "Food", diff --git a/Content.Client/UserInterface/GhostGui.cs b/Content.Client/UserInterface/GhostGui.cs index 5ac6e1a7cc..b998597790 100644 --- a/Content.Client/UserInterface/GhostGui.cs +++ b/Content.Client/UserInterface/GhostGui.cs @@ -2,12 +2,14 @@ using Content.Client.GameObjects.Components.Observer; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.IoC; +using Robust.Shared.Localization; namespace Content.Client.UserInterface { public class GhostGui : Control { - public Button ReturnToBody = new Button(){Text = "Return to body"}; + + public readonly Button ReturnToBody = new Button() {Text = Loc.GetString("Return to body")}; private GhostComponent _owner; public GhostGui(GhostComponent owner) diff --git a/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs new file mode 100644 index 0000000000..4eefd651e3 --- /dev/null +++ b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs @@ -0,0 +1,235 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Observer; +using Content.Server.GameObjects.Components.Power.ApcNetComponents; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Server.Mobs; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Medical; +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Shared.Preferences; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Medical +{ + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + public class CloningPodComponent : SharedCloningPodComponent, IActivate + { + [Dependency] private readonly IServerPreferencesManager _prefsManager = null!; + [Dependency] private readonly IEntityManager _entityManager = null!; + [Dependency] private readonly IPlayerManager _playerManager = null!; + + [ViewVariables] + private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; + + [ViewVariables] + private BoundUserInterface? UserInterface => + Owner.GetUIOrNull(CloningPodUIKey.Key); + + private ContainerSlot _bodyContainer = default!; + private Mind? _capturedMind; + private CloningPodStatus _status; + private float _cloningProgress = 0; + private float _cloningTime; + + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _cloningTime, "cloningTime", 10f); + } + + public override void Initialize() + { + base.Initialize(); + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiReceiveMessage; + } + + _bodyContainer = ContainerManagerComponent.Ensure($"{Name}-bodyContainer", Owner); + + //TODO: write this so that it checks for a change in power events for GORE POD cases + var newState = GetUserInterfaceState(); + UserInterface?.SetState(newState); + + UpdateUserInterface(); + + Owner.EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, + HandleGhostReturn); + } + + public void Update(float frametime) + { + if (_bodyContainer.ContainedEntity != null && + Powered) + { + _cloningProgress += frametime; + _cloningProgress = MathHelper.Clamp(_cloningProgress, 0f, _cloningTime); + } + + if (_cloningProgress >= _cloningTime && + _bodyContainer.ContainedEntity != null && + _capturedMind?.Session.AttachedEntity == _bodyContainer.ContainedEntity && + Powered) + { + _bodyContainer.Remove(_bodyContainer.ContainedEntity); + _capturedMind = null; + _cloningProgress = 0f; + + _status = CloningPodStatus.Idle; + UpdateAppearance(); + } + + UpdateUserInterface(); + } + + public override void OnRemove() + { + if (UserInterface != null) + { + UserInterface.OnReceiveMessage -= OnUiReceiveMessage; + } + + Owner.EntityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); + + base.OnRemove(); + } + + private void UpdateUserInterface() + { + if (!Powered) return; + + UserInterface?.SetState(GetUserInterfaceState()); + } + + private CloningPodBoundUserInterfaceState GetUserInterfaceState() + { + return new CloningPodBoundUserInterfaceState(CloningSystem.getIdToUser(), _cloningProgress, + (_status == CloningPodStatus.Cloning)); + } + + private void UpdateAppearance() + { + if (Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(CloningPodVisuals.Status, _status); + } + } + + public void Activate(ActivateEventArgs eventArgs) + { + if (!Powered || + !eventArgs.User.TryGetComponent(out IActorComponent? actor)) + { + return; + } + + UserInterface?.Open(actor.playerSession); + } + + private async void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) + { + if (!(obj.Message is CloningPodUiButtonPressedMessage message)) return; + + switch (message.Button) + { + case UiButton.Clone: + + if (message.ScanId == null) return; + + if (_bodyContainer.ContainedEntity != null || + !CloningSystem.Minds.TryGetValue(message.ScanId.Value, out var mind)) + { + return; + } + + var dead = + mind.OwnedEntity.TryGetComponent(out var damageable) && + damageable.CurrentDamageState == DamageState.Dead; + if (!dead) return; + + + var mob = _entityManager.SpawnEntity("HumanMob_Content", Owner.Transform.MapPosition); + var client = _playerManager + .GetPlayersBy(x => x.SessionId == mind.SessionId).First(); + mob.GetComponent() + .UpdateFromProfile(GetPlayerProfileAsync(client.Name).Result); + mob.Name = GetPlayerProfileAsync(client.Name).Result.Name; + + _bodyContainer.Insert(mob); + _capturedMind = mind; + + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, + new CloningStartedMessage(_capturedMind)); + _status = CloningPodStatus.NoMind; + UpdateAppearance(); + + break; + + case UiButton.Eject: + if (_bodyContainer.ContainedEntity == null || _cloningProgress < _cloningTime) break; + + _bodyContainer.Remove(_bodyContainer.ContainedEntity!); + _capturedMind = null; + _cloningProgress = 0f; + _status = CloningPodStatus.Idle; + UpdateAppearance(); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public class CloningStartedMessage : EntitySystemMessage + { + public CloningStartedMessage(Mind capturedMind) + { + CapturedMind = capturedMind; + } + + public Mind CapturedMind { get; } + } + + + private async Task GetPlayerProfileAsync(string username) + { + return (HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(username)) + .SelectedCharacter; + } + + private void HandleGhostReturn(GhostComponent.GhostReturnMessage message) + { + if (message.Sender == _capturedMind) + { + //If the captured mind is in a ghost, we want to get rid of it. + _capturedMind.VisitingEntity?.Delete(); + + //Transfer the mind to the new mob + _capturedMind.TransferTo(_bodyContainer.ContainedEntity); + + _status = CloningPodStatus.Cloning; + UpdateAppearance(); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs index cb2a11fb40..5a297b71c2 100644 --- a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs @@ -1,9 +1,14 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Body; +using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Power.ApcNetComponents; using Content.Server.GameObjects.EntitySystems; +using Content.Server.Players; using Content.Server.Utility; +using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Medical; using Content.Shared.GameObjects.EntitySystems; @@ -13,21 +18,24 @@ using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components.Container; using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Maths; -using Content.Shared.Damage; +using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Maths; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Medical { [RegisterComponent] [ComponentReference(typeof(IActivate))] - public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate + public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate, IDragDropOn { private ContainerSlot _bodyContainer = default!; private readonly Vector2 _ejectOffset = new Vector2(-0.5f, 0f); + + [Dependency] private readonly IPlayerManager _playerManager = null!; public bool IsOccupied => _bodyContainer.ContainedEntity != null; [ViewVariables] @@ -68,13 +76,12 @@ namespace Content.Server.GameObjects.Components.Medical if (Owner.TryGetComponent(out AppearanceComponent? appearance)) { appearance?.SetData(MedicalScannerVisuals.Status, MedicalScannerStatus.Open); - }; + } return EmptyUIState; } - if (!body.TryGetComponent(out IDamageableComponent? damageable) || - damageable.CurrentDamageState == DamageState.Dead) + if (!body.TryGetComponent(out IDamageableComponent? damageable)) { return EmptyUIState; } @@ -82,7 +89,14 @@ namespace Content.Server.GameObjects.Components.Medical var classes = new Dictionary(damageable.DamageClasses); var types = new Dictionary(damageable.DamageTypes); - return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, CloningSystem.HasUid(body.Uid)); + if (_bodyContainer.ContainedEntity?.Uid == null) + { + return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, true); + } + + + return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, + CloningSystem.HasDnaScan(_bodyContainer.ContainedEntity.GetComponent().Mind)); } private void UpdateUserInterface() @@ -207,22 +221,41 @@ namespace Content.Server.GameObjects.Components.Medical private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) { - if (!(obj.Message is UiButtonPressedMessage message)) - { - return; - } + if (!(obj.Message is UiButtonPressedMessage message)) return; switch (message.Button) { case UiButton.ScanDNA: if (_bodyContainer.ContainedEntity != null) { - CloningSystem.AddToScannedUids(_bodyContainer.ContainedEntity.Uid); + //TODO: Show a 'ERROR: Body is completely devoid of soul' if no Mind owns the entity. + CloningSystem.AddToDnaScans(_playerManager + .GetPlayersBy(playerSession => + { + var mindOwnedMob = playerSession.ContentData()?.Mind?.OwnedEntity; + + return mindOwnedMob != null && mindOwnedMob == + _bodyContainer.ContainedEntity; + }).Single() + .ContentData() + ?.Mind); } + break; default: throw new ArgumentOutOfRangeException(); } } + + public bool CanDragDropOn(DragDropEventArgs eventArgs) + { + return eventArgs.Dropped.HasComponent(); + } + + public bool DragDropOn(DragDropEventArgs eventArgs) + { + _bodyContainer.Insert(eventArgs.Dropped); + return true; + } } } diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index b5f9eba7e1..9c199a2cd9 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -1,9 +1,16 @@ #nullable enable +using System; +using Content.Server.GameObjects.Components.Body; +using Content.Server.GameObjects.Components.Medical; using Content.Server.GameObjects.Components.Observer; using Content.Server.Interfaces.GameTicking; using Content.Server.Mobs; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.EntitySystems; +using Robust.Server.GameObjects.Components.UserInterface; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; @@ -14,6 +21,7 @@ using Robust.Shared.Serialization; using Robust.Shared.Timers; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; +using Serilog.Debugging; namespace Content.Server.GameObjects.Components.Mobs { @@ -50,6 +58,45 @@ namespace Content.Server.GameObjects.Components.Mobs set => _showExamineInfo = value; } + [ViewVariables] + private BoundUserInterface? UserInterface => + Owner.GetUIOrNull(SharedAcceptCloningComponent.AcceptCloningUiKey.Key); + + + public override void Initialize() + { + base.Initialize(); + Owner.EntityManager.EventBus.SubscribeEvent( + EventSource.Local, this, + HandleCloningStartedMessage); + + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiAcceptCloningMessage; + } + } + + private void HandleCloningStartedMessage(CloningPodComponent.CloningStartedMessage ev) + { + if (ev.CapturedMind == Mind) + { + UserInterface?.Open(Mind.Session); + } + } + + private void OnUiAcceptCloningMessage(ServerBoundUserInterfaceMessage obj) + { + if (!(obj.Message is SharedAcceptCloningComponent.UiButtonPressedMessage message)) return; + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new GhostComponent.GhostReturnMessage(Mind)); + } + + public override void OnRemove() + { + base.OnRemove(); + Owner.EntityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); + if (UserInterface != null) UserInterface.OnReceiveMessage -= OnUiAcceptCloningMessage; + } + /// /// Don't call this unless you know what the hell you're doing. /// Use instead. @@ -133,13 +180,19 @@ namespace Content.Server.GameObjects.Components.Mobs if (!HasMind) { message.AddMarkup(!dead - ? $"[color=red]" + Loc.GetString("{0:They} {0:are} totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", Owner) + "[/color]" + ? $"[color=red]" + + Loc.GetString( + "{0:They} {0:are} totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", + Owner) + "[/color]" : $"[color=purple]" + Loc.GetString("{0:Their} soul has departed.", Owner) + "[/color]"); } else if (Mind?.Session == null) { - if(!dead) - message.AddMarkup("[color=yellow]" + Loc.GetString("{0:They} {0:have} a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", Owner) + "[/color]"); + if (!dead) + message.AddMarkup("[color=yellow]" + + Loc.GetString( + "{0:They} {0:have} a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", + Owner) + "[/color]"); } } } diff --git a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs index cb0c938419..10f3e8a6c6 100644 --- a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs +++ b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs @@ -1,4 +1,6 @@ -using Content.Server.Players; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.Mobs; +using Content.Server.Players; using Content.Shared.GameObjects.Components.Observer; using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components; @@ -31,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Observer { base.Initialize(); - Owner.EnsureComponent().Layer = (int)VisibilityFlags.Ghost; + Owner.EnsureComponent().Layer = (int) VisibilityFlags.Ghost; } public override ComponentState GetComponentState() => new GhostComponentState(CanReturnToBody); @@ -43,18 +45,19 @@ namespace Content.Server.GameObjects.Components.Observer switch (message) { case PlayerAttachedMsg msg: - msg.NewPlayer.VisibilityMask |= (int)VisibilityFlags.Ghost; + msg.NewPlayer.VisibilityMask |= (int) VisibilityFlags.Ghost; Dirty(); break; case PlayerDetachedMsg msg: - msg.OldPlayer.VisibilityMask &= ~(int)VisibilityFlags.Ghost; + msg.OldPlayer.VisibilityMask &= ~(int) VisibilityFlags.Ghost; break; default: break; } } - public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, + ICommonSession session = null) { base.HandleNetworkMessage(message, netChannel, session); @@ -67,10 +70,29 @@ namespace Content.Server.GameObjects.Components.Observer actor.playerSession.ContentData().Mind.UnVisit(); Owner.Delete(); } + + break; + case ReturnToCloneComponentMessage reenter: + + if (Owner.TryGetComponent(out VisitingMindComponent mind)) + { + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new GhostReturnMessage(mind.Mind)); + } + break; default: break; } } + + public class GhostReturnMessage : EntitySystemMessage + { + public GhostReturnMessage(Mind sender) + { + Sender = sender; + } + + public Mind Sender { get; } + } } } diff --git a/Content.Server/GameObjects/EntitySystems/CloningSystem.cs b/Content.Server/GameObjects/EntitySystems/CloningSystem.cs index 469c0f6157..e9bb60a850 100644 --- a/Content.Server/GameObjects/EntitySystems/CloningSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/CloningSystem.cs @@ -1,24 +1,42 @@ using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Medical; +using Content.Server.Mobs; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; +using Content.Shared.GameObjects.Components.Medical; +using Microsoft.AspNetCore.Server.Kestrel.Core; namespace Content.Server.GameObjects.EntitySystems { internal sealed class CloningSystem : EntitySystem { - public static List scannedUids = new List(); - - public static void AddToScannedUids(EntityUid uid) + public override void Update(float frameTime) { - if (!scannedUids.Contains(uid)) + foreach (var comp in ComponentManager.EntityQuery()) { - scannedUids.Add(uid); + comp.Update(frameTime); } } - public static bool HasUid(EntityUid uid) + public static Dictionary Minds = new Dictionary(); + + public static void AddToDnaScans(Mind mind) { - return scannedUids.Contains(uid); + if (!Minds.ContainsValue(mind)) + { + Minds.Add(Minds.Count(), mind); + } + } + + public static bool HasDnaScan(Mind mind) + { + return Minds.ContainsValue(mind); + } + + public static Dictionary getIdToUser() + { + return Minds.ToDictionary(m => m.Key, m => m.Value.CharacterName); } } } diff --git a/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs b/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs new file mode 100644 index 0000000000..6c0ed3f87e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Medical +{ + + public class SharedCloningPodComponent : Component + { + public override string Name => "CloningPod"; + + [Serializable, NetSerializable] + public class CloningPodBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly Dictionary MindIdName; + public readonly float Progress; + public readonly bool MindPresent; + + public CloningPodBoundUserInterfaceState(Dictionary mindIdName, float progress, bool mindPresent) + { + MindIdName = mindIdName; + Progress = progress; + MindPresent = mindPresent; + } + } + + + [Serializable, NetSerializable] + public enum CloningPodUIKey + { + Key + } + + [Serializable, NetSerializable] + public enum CloningPodVisuals + { + Status + } + + [Serializable, NetSerializable] + public enum CloningPodStatus + { + Idle, + Cloning, + Gore, + NoMind + } + + [Serializable, NetSerializable] + public enum UiButton + { + Clone, + Eject + } + + [Serializable, NetSerializable] + public class CloningPodUiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + public readonly int? ScanId; + + public CloningPodUiButtonPressedMessage(UiButton button, int? scanId) + { + Button = button; + ScanId = scanId; + } + } + + } +} diff --git a/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs b/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs index 771c0b0b6f..e9a029271c 100644 --- a/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs +++ b/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs @@ -35,4 +35,11 @@ namespace Content.Shared.GameObjects.Components.Observer { public ReturnToBodyComponentMessage() => Directed = true; } + + + [Serializable, NetSerializable] + public class ReturnToCloneComponentMessage : ComponentMessage + { + public ReturnToCloneComponentMessage() => Directed = true; + } } diff --git a/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs b/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs new file mode 100644 index 0000000000..94383004c5 --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs @@ -0,0 +1,36 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components +{ + public class SharedAcceptCloningComponent : Component + { + public override string Name => "AcceptCloning"; + + [Serializable, NetSerializable] + public enum AcceptCloningUiKey + { + Key + } + + [Serializable, NetSerializable] + public enum UiButton + { + Accept + } + + [Serializable, NetSerializable] + public class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + + public UiButtonPressedMessage(UiButton button) + { + Button = button; + } + } + + } +} diff --git a/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml b/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml new file mode 100644 index 0000000000..11125bcc92 --- /dev/null +++ b/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml @@ -0,0 +1,44 @@ +- type: entity + id: CloningPod + name: Cloning Pod + description: A Cloning Pod. 50% reliable. + components: + - type: Sprite + netsync: false + sprite: Objects/Specific/Medical/cloning.rsi + layers: + - state: pod_0 + map: ["enum.CloningPodVisualLayers.Machine"] + - type: PowerReceiver + - type: Icon + sprite: Objects/Specific/Medical/cloning.rsi + state: pod_0 + - type: Anchorable + - type: Clickable + - type: InteractionOutline + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.5,-0.25,0.5,0.25" + layer: + - Opaque + - Impassable + - MobImpassable + - VaultImpassable + IsScrapingFloor: true + - type: Physics + mass: 25 + anchored: true + - type: SnapGrid + offset: Center + - type: CloningPod + cloningTime: 10.0 + - type: Destructible + deadThreshold: 100 + - type: Appearance + visuals: + - type: CloningPodVisualizer + - type: UserInterface + interfaces: + - key: enum.CloningPodUIKey.Key + type: CloningPodBoundUserInterface diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 43a4507138..f309bea3d5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -92,7 +92,7 @@ state: r_foot - map: ["enum.HumanoidVisualLayers.Handcuffs"] color: "#ffffff" - sprite: Objects/Misc/handcuffs.rsi + sprite: Objects/Misc/handcuffs.rsi state: body-overlay-2 visible: false - map: ["enum.Slots.IDCARD"] @@ -169,6 +169,8 @@ interfaces: - key: enum.StrippingUiKey.Key type: StrippableBoundUserInterface + - key: enum.AcceptCloningUiKey.Key + type: AcceptCloningBoundUserInterface - type: entity @@ -230,7 +232,7 @@ state: r_hand - map: ["enum.HumanoidVisualLayers.Handcuffs"] color: "#ffffff" - sprite: Objects/Misc/handcuffs.rsi + sprite: Objects/Misc/handcuffs.rsi state: body-overlay-2 visible: false - map: ["enum.Slots.IDCARD"] diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/meta.json similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/meta.json rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/meta.json diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/pod_0.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/pod_0.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/pod_0.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/pod_0.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_maintenance.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_maintenance.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_maintenance.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_maintenance.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_occupied.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_occupied.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_occupied.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_occupied.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_maintenance.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_maintenance.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_maintenance.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_maintenance.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_unpowered.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_unpowered.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_unpowered.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_unpowered.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_unpowered.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_unpowered.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_unpowered.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_unpowered.png diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json new file mode 100644 index 0000000000..ac8957b3fe --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC BY-SA 3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation at commit 9bebd81ae0b0a7f952b59886a765c681205de31f", + "states": [ + { + "name": "pod_0", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "pod_1", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "pod_e", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "pod_g", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png new file mode 100644 index 0000000000000000000000000000000000000000..13d289fd6cc923117d0ff7e9e6e78dc6fa29f990 GIT binary patch literal 1319 zcmV+?1=#wDP)kdTld!5zkG*y4t*8g!ux zT_8I#0gVD7DMZ>LrBCqz0)37%(|O#PxsUIDF79+{Tj*SpCVGk6Wg2o13a!jt69B7h7E z5eQ)K{=@R8b6+re?-4sY+W>G}!L`02as`u6C`7y=%9fV(#Ofj(J#r*4L`{G>@mphm zup!Z05qEj6B|&Rq4Oe?75T3{Qw95E&isv`C(iCstVYwm@Kp+Q-Wt+)ln(G6jtZj_*=feqpywO)_I29KgMfIIF%_8ZXi4fij z0+cKOhR1&6L4Aw|6O-igd88)+fsh`WZXl(^c5HG56IZOL17-52iGd(rFiFkka2!N!Ln_N<2ZPpBosF06Qpmq^dwSBJkO(S+kpTAmB14&CEN8EXeyor+j1)6AW;yGi>4V> z7lKA02a1-BW7~+z86aG@Dh}Xzz5|M)peP!`leliMDF_5mFiTjjTRDV$F;!J7-3OZP zm&0#LpQ=zS1_B6F0@wBJ)-?@04>8yMYK31>7UjE?=Ss8^0CvSrR|s6s!?qoN3IIjb z0MHEGcLfFM3CiWt$^nFQ6&m7fNHn7BI+kr$#6U_9JrqJy^-2RuK*21pumjOZn1I{|7rn>!=^$YX5l=o4!!0$Jp#urgy+wSTq^Tt~T*tC8vr+N zjj5rYW%m!%vah_hk1dI32pI;c%q-J?r|G?MV_whD+0?d`STu$ntw&1G41>bdy_$PJ z7GUF!SEXh7Z?j}FMKYP9dCwknO=EmINj6tlvc|DW5RKt^0_jN}j12(ulN1hz_^bcQ zl5#8nm`i@|=t)M$Mwp(S;^6)_u^pGwpPZ@YHLYvKFbpD*NW}qz#&xYc{B>x4uexUe z$e8!p*t!`ZG_1=0|L%(gM2T&?<*5@Na{KlzCMPE;ni&8rtH6PSN3pCj;c$e{&z}Y0 zH7PM%n-t|~hIa0~$3vM80ZD0-OY#OJyST+d9tmav^MK}~DpU)8rhp{b-4IBMGFn50+#Eq?5VEYpR9v5>^AH37erAyz@ z)%7|TFJ7R%y_3<=VcOd}863JpJRWDDzlYB4I~X49Cs&z-T4Naa^JTF+Pm1Q|Mv}>N dP2vAd`~xa+a~}W~hi(7>002ovPDHLkV1hLkWOV=l literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png new file mode 100644 index 0000000000000000000000000000000000000000..eef65e1aeffe29f30f8a2aa5e757bbc161e19be4 GIT binary patch literal 2967 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 N002ovPDHLkV1g*G)WHA% literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png new file mode 100644 index 0000000000000000000000000000000000000000..acd9b6c194df553e686defd80188363dad3f3c4e GIT binary patch literal 2937 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3e002156e3fd72d7c0b86c54f6d3ba22e86259 GIT binary patch literal 3907 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