From 917ade35aa6fbb881b359d268a7b2e3e1b016233 Mon Sep 17 00:00:00 2001 From: gluesniffler Date: Sat, 2 Nov 2024 03:37:24 -0400 Subject: [PATCH 1/9] Added some experimental lag fixes to surgery. Might have crashes --- Content.Client/Body/Systems/BodySystem.cs | 13 +++ Content.Client/Medical/Surgery/SurgeryBui.cs | 69 ++++++--------- .../Medical/Surgery/SurgerySystem.cs | 15 +++- .../Medical/Surgery/SurgeryWindow.xaml | 7 -- Content.Server/Body/Systems/BodySystem.cs | 5 ++ .../Medical/Surgery/SurgerySystem.cs | 17 +++- .../Body/Part/BodyPartAppearanceComponent.cs | 27 ++++++ .../Body/Systems/SharedBodySystem.Body.cs | 16 +++- .../Systems/SharedBodySystem.Integrity.cs | 4 +- .../SharedBodySystem.PartAppearance.cs | 83 +++++++++++++++++++ .../Body/Systems/SharedBodySystem.Parts.cs | 12 +++ .../Surgery/SharedSurgerySystem.Steps.cs | 4 +- .../Medical/Surgery/SharedSurgerySystem.cs | 21 +++-- .../Medical/Surgery/SurgeryTargetComponent.cs | 2 +- .../Medical/Surgery/SurgeryUiRefreshEvent.cs | 14 ++++ Resources/Locale/en-US/surgery/surgery-ui.ftl | 2 +- .../Entities/Structures/Machines/lathe.yml | 3 +- .../Prototypes/Recipes/Lathes/medical.yml | 2 +- 18 files changed, 242 insertions(+), 74 deletions(-) create mode 100644 Content.Shared/Body/Part/BodyPartAppearanceComponent.cs create mode 100644 Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs create mode 100644 Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs index bab785525b0dc7..52af9d93493fd4 100644 --- a/Content.Client/Body/Systems/BodySystem.cs +++ b/Content.Client/Body/Systems/BodySystem.cs @@ -1,7 +1,20 @@ using Content.Shared.Body.Systems; +using Content.Shared.Body.Part; +using Robust.Client.GameObjects; namespace Content.Client.Body.Systems; public sealed class BodySystem : SharedBodySystem { + protected override void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component) + { + if (TryComp(uid, out SpriteComponent? sprite)) + { + if (component.Color != null) + { + //TODO a few things need to be adjusted before this is ready to be used - also need to find a way to update the player sprite + //sprite.Color = component.Color.Value; + } + } + } } diff --git a/Content.Client/Medical/Surgery/SurgeryBui.cs b/Content.Client/Medical/Surgery/SurgeryBui.cs index ddfbf561f2b02d..5cebffb0914704 100644 --- a/Content.Client/Medical/Surgery/SurgeryBui.cs +++ b/Content.Client/Medical/Surgery/SurgeryBui.cs @@ -5,12 +5,14 @@ using Content.Shared.Body.Part; using Content.Shared.Rotation; using Content.Shared.Standing; +using Content.Client.Hands.Systems; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Robust.Shared.Timing; +using Robust.Client.Timing; using static Robust.Client.UserInterface.Control; namespace Content.Client.Medical.Surgery; @@ -21,10 +23,11 @@ public sealed class SurgeryBui : BoundUserInterface [Dependency] private readonly IEntityManager _entities = default!; [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IClientGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _timing = default!; private readonly SurgerySystem _system; - + private readonly HandsSystem _hands; [ViewVariables] private SurgeryWindow? _window; @@ -32,22 +35,30 @@ public sealed class SurgeryBui : BoundUserInterface private bool _isBody = false; private (EntityUid Ent, EntProtoId Proto)? _surgery; private readonly List _previousSurgeries = new(); - + private DateTime _lastRefresh = DateTime.UtcNow; + private (string handName, EntityUid item) _throttling = ("", new EntityUid()); public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) { _system = _entities.System(); + _hands = _entities.System(); + + _system.OnStep += RefreshUI; + _hands.OnPlayerItemAdded += OnPlayerItemAdded; } - protected override void Open() + private void OnPlayerItemAdded(string handName, EntityUid item) { - _system.OnRefresh += () => - { - UpdateDisabledPanel(); - RefreshUI(); - }; + if (_throttling.handName.Equals(handName) + && _throttling.item.Equals(item) + && DateTime.UtcNow - _lastRefresh < TimeSpan.FromSeconds(0.2) + || !_timing.IsFirstTimePredicted + || _window == null + || !_window.IsOpen) + return; - if (State is SurgeryBuiState s) - Update(s); + _throttling = (handName, item); + _lastRefresh = DateTime.UtcNow; + RefreshUI(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -59,11 +70,10 @@ protected override void UpdateState(BoundUserInterfaceState state) protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (disposing) _window?.Dispose(); - _system.OnRefresh -= RefreshUI; + _system.OnStep -= RefreshUI; } private void Update(SurgeryBuiState state) @@ -71,6 +81,7 @@ private void Update(SurgeryBuiState state) if (!_entities.TryGetComponent(_player.LocalEntity, out var surgeryTargetComp) || !surgeryTargetComp.CanOperate) return; + if (_window == null) { _window = new SurgeryWindow(); @@ -125,7 +136,6 @@ State is not SurgeryBuiState s || _window.Surgeries.DisposeAllChildren(); _window.Steps.DisposeAllChildren(); _window.Parts.DisposeAllChildren(); - View(ViewType.Parts); var oldSurgery = _surgery; @@ -194,11 +204,6 @@ int GetScore(BodyPartType? partType) if (!_window.IsOpen) _window.OpenCentered(); - else - { - RefreshUI(); - UpdateDisabledPanel(); - } } private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId) @@ -303,7 +308,6 @@ private void OnPartPressed(NetEntity netPart, List surgeryIds) private void RefreshUI() { if (_window == null - || !_timing.IsFirstTimePredicted || !_window.IsOpen || _part == null || !_entities.HasComponent(_surgery?.Ent) @@ -312,7 +316,7 @@ private void RefreshUI() { return; } - + Logger.Debug($"Running RefreshUI on {Owner}"); var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent); var i = 0; foreach (var child in _window.Steps.Children) @@ -350,9 +354,6 @@ private void RefreshUI() else { stepButton.Button.Modulate = Color.White; - // GOD THIS NEEDS A REWRITE SO BADLY, IT UPDATES ON EVERY SINGLE TICK - // THEN RUNS CANPERFORMSTEP WHICH CALLS A SHITLOAD OF EVENTS - // DID THEY NOT FUCKING PLAYTEST THIS??? if (_player.LocalEntity is { } player && status == StepStatus.Next && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _)) @@ -382,28 +383,6 @@ private void RefreshUI() stepButton.Set(stepName, texture); i++; } - - UpdateDisabledPanel(); - } - - private void UpdateDisabledPanel() - { - if (_window == null) - return; - - if (_system.IsLyingDown(Owner)) - { - _window.DisabledPanel.Visible = false; - _window.DisabledPanel.MouseFilter = MouseFilterMode.Ignore; - return; - } - - _window.DisabledPanel.Visible = true; - - var text = new FormattedMessage(); - text.AddMarkup($"[color=red][font size=16]{Loc.GetString("surgery-ui-window-steps-error-laying")}[/font][/color]"); - _window.DisabledLabel.SetMessage(text); - _window.DisabledPanel.MouseFilter = MouseFilterMode.Stop; } private void View(ViewType type) diff --git a/Content.Client/Medical/Surgery/SurgerySystem.cs b/Content.Client/Medical/Surgery/SurgerySystem.cs index 19c8fac5fce399..73af1d85b9e855 100644 --- a/Content.Client/Medical/Surgery/SurgerySystem.cs +++ b/Content.Client/Medical/Surgery/SurgerySystem.cs @@ -4,10 +4,17 @@ namespace Content.Client.Medical.Surgery; public sealed class SurgerySystem : SharedSurgerySystem { - public event Action? OnRefresh; + public event Action? OnStep; - public override void Update(float frameTime) + public override void Initialize() { - OnRefresh?.Invoke(); + base.Initialize(); + + SubscribeNetworkEvent(OnRefresh); + } + + private void OnRefresh(SurgeryUiRefreshEvent ev) + { + OnStep?.Invoke(); } -} \ No newline at end of file +} diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml b/Content.Client/Medical/Surgery/SurgeryWindow.xaml index fc66e0cd71be2b..bba801a8a58e9a 100644 --- a/Content.Client/Medical/Surgery/SurgeryWindow.xaml +++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml @@ -20,11 +20,4 @@ - - - - - - diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 441dc560a0681a..24116b8e3b7b77 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -165,4 +165,9 @@ public override HashSet GibPart( return gibs; } + + protected override void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component) + { + return; + } } diff --git a/Content.Server/Medical/Surgery/SurgerySystem.cs b/Content.Server/Medical/Surgery/SurgerySystem.cs index e96bc3229a665f..850ce1430a3449 100644 --- a/Content.Server/Medical/Surgery/SurgerySystem.cs +++ b/Content.Server/Medical/Surgery/SurgerySystem.cs @@ -81,6 +81,18 @@ protected override void RefreshUI(EntityUid body) } _ui.SetUiState(body, SurgeryUIKey.Key, new SurgeryBuiState(surgeries)); + /* + Reason we do this is because when applying a BUI State, it rolls back the state on the entity temporarily, + which just so happens to occur right as we're checking for step completion, so we end up with the UI + not updating at all until you change tools or reopen the window. + */ + + var actors = _ui.GetActors(body, SurgeryUIKey.Key).ToArray(); + if (actors.Length == 0) + return; + + var filter = Filter.Entities(actors); + RaiseNetworkEvent(new SurgeryUiRefreshEvent(GetNetEntity(body)), filter); } private void SetDamage(EntityUid body, DamageSpecifier damage, float partMultiplier, @@ -104,7 +116,8 @@ private void OnToolAfterInteract(Entity ent, ref AfterInte || !args.CanReach || args.Target == null || !TryComp(args.User, out var surgery) - || !surgery.CanOperate) + || !surgery.CanOperate + || !IsLyingDown(args.Target.Value, args.User)) { return; } @@ -176,7 +189,7 @@ private void OnStepAffixPartComplete(Entity var ev = new BodyPartEnableChangedEvent(true); RaiseLocalEvent(targetPart.Id, ref ev); // This is basically an equalizer, severing a part will badly damage it. - // and affixing it will heal it a bit if its not too badly damaged. + // and affixing it will heal it a bit if it's not too badly damaged. _body.TryChangeIntegrity(targetPart, targetPart.Component.Integrity - 20, false, _body.GetTargetBodyPart(targetPart.Component.PartType, targetPart.Component.Symmetry), out _); } diff --git a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs new file mode 100644 index 00000000000000..5c9aa5ba60a267 --- /dev/null +++ b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.GameStates; + +namespace Content.Shared.Body.Part; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class BodyPartAppearanceComponent : Component +{ + + /// + /// ID of this custom base layer. Must be a . + /// + [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] + public string? ID { get; set; } + + /// + /// Color of this custom base layer. Null implies skin colour if the corresponding is set to match skin. + /// + [DataField("color"), AutoNetworkedField] + public Color? Color { get; set; } + + [DataField("originalBody"), AutoNetworkedField] + public EntityUid? OriginalBody { get; set; } + + //TODO add other custom variables such as species and markings - in case someone decides to attach a lizard arm to a human for example +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 558a190f1ca506..29409c6b23dc7a 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -10,6 +10,7 @@ using Content.Shared.Gibbing.Components; using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Standing; @@ -19,7 +20,6 @@ using Robust.Shared.Map; using Robust.Shared.Utility; using Robust.Shared.Timing; - namespace Content.Shared.Body.Systems; public partial class SharedBodySystem @@ -219,6 +219,20 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) continue; } + if (TryComp(parentPartComponent.Body, out HumanoidAppearanceComponent? bodyAppearance)) + { + var appearance = AddComp(childPart); + appearance.OriginalBody = childPartComponent.OriginalBody; + appearance.Color = bodyAppearance.SkinColor; + + var symmetry = ((BodyPartSymmetry) childPartComponent.Symmetry).ToString(); + if (symmetry == "None") + symmetry = ""; + appearance.ID = "removed" + symmetry + ((BodyPartType) childPartComponent.PartType).ToString(); + + Dirty(childPart, appearance); + } + // Add organs SetupOrgans((childPart, childPartComponent), connectionSlot.Organs); diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs index 415295561a312e..d7d6e7749758a4 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs @@ -134,12 +134,15 @@ public void TryChangeIntegrity(Entity partEnt, out bool severed) { severed = false; + if (!_timing.IsFirstTimePredicted || !_queryTargeting.HasComp(partEnt.Comp.Body)) return; var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot; var originalIntegrity = partEnt.Comp.Integrity; partEnt.Comp.Integrity = Math.Min(BodyPartComponent.MaxIntegrity, partEnt.Comp.Integrity - integrity); + + // This will also prevent the torso from being removed. if (canSever && !HasComp(partEnt) && !partEnt.Comp.Enabled @@ -147,7 +150,6 @@ public void TryChangeIntegrity(Entity partEnt, && partIdSlot is not null) severed = true; - // This will also prevent the torso from being removed. if (partEnt.Comp.Enabled && partEnt.Comp.Integrity <= 15.0f) { diff --git a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs new file mode 100644 index 00000000000000..930da554379c1c --- /dev/null +++ b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs @@ -0,0 +1,83 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Body.Events; +using Robust.Shared.GameStates; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; + +namespace Content.Shared.Body.Systems; +// Code shamelessly stolen from MS14. +public partial class SharedBodySystem +{ + private void InitializePartAppearances() + { + base.Initialize(); + + SubscribeLocalEvent(OnPartAppearanceInit); + SubscribeLocalEvent(OnPartAddedToBody); + } + + private static readonly Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> BodyPartVisualLayers + = new Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> + { + { (BodyPartType.Head,BodyPartSymmetry.None), HumanoidVisualLayers.Head }, + { (BodyPartType.Tail,BodyPartSymmetry.None), HumanoidVisualLayers.Tail }, + { (BodyPartType.Torso,BodyPartSymmetry.None), HumanoidVisualLayers.Chest }, + { (BodyPartType.Arm,BodyPartSymmetry.Right), HumanoidVisualLayers.RArm }, + { (BodyPartType.Arm,BodyPartSymmetry.Left), HumanoidVisualLayers.LArm }, + { (BodyPartType.Hand,BodyPartSymmetry.Right), HumanoidVisualLayers.RHand }, + { (BodyPartType.Hand,BodyPartSymmetry.Left), HumanoidVisualLayers.LHand }, + { (BodyPartType.Leg,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, + { (BodyPartType.Leg,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg }, + { (BodyPartType.Foot,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, + { (BodyPartType.Foot,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg } + }; + + private void OnPartAppearanceInit(EntityUid uid, BodyPartAppearanceComponent component, ComponentInit args) + { + + if (TryComp(uid, out BodyPartComponent? part) && part.OriginalBody != null && + TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance)) + { + var customLayers = bodyAppearance.CustomBaseLayers; + var spriteLayers = bodyAppearance.BaseLayers; + var visualLayer = BodyPartVisualLayers[(part.PartType, part.Symmetry)]; + + component.OriginalBody = part.OriginalBody.Value; + + if (customLayers.ContainsKey(visualLayer)) + { + component.ID = customLayers[visualLayer].Id; + component.Color = customLayers[visualLayer].Color; + } + else if (spriteLayers.ContainsKey(visualLayer)) + { + component.ID = spriteLayers[visualLayer].ID; + component.Color = bodyAppearance.SkinColor; + } + else + { + var symmetry = ((BodyPartSymmetry) part.Symmetry).ToString(); + if (symmetry == "None") + symmetry = ""; + component.ID = "removed" + symmetry + ((BodyPartType) part.PartType).ToString(); + component.Color = bodyAppearance.SkinColor; + } + } + Dirty(uid, component); + UpdateAppearance(uid, component); + } + + public void OnPartAddedToBody(EntityUid uid, BodyPartAppearanceComponent component, ref BodyPartAddedEvent args) + { + if (TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance)) + { + var part = args.Part; + var customLayers = bodyAppearance.CustomBaseLayers; + var visualLayer = BodyPartVisualLayers[(part.Comp.PartType, part.Comp.Symmetry)]; + customLayers[visualLayer] = new CustomBaseLayerInfo(component.ID, customLayers[visualLayer].Color); + } + } + + protected abstract void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component); +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index 06acd3d855218b..2e1b39a480908c 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -4,6 +4,7 @@ using Content.Shared.Body.Part; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Random; @@ -29,6 +30,7 @@ private void InitializeParts() SubscribeLocalEvent(OnBodyPartInserted); SubscribeLocalEvent(OnBodyPartRemoved); SubscribeLocalEvent(OnAmputateAttempt); + SubscribeLocalEvent(OnPartEnableChanged); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -601,6 +603,16 @@ public bool AttachPart( } part.ParentSlot = slot; + + if (TryComp(part.Body, out HumanoidAppearanceComponent? bodyAppearance) + && !HasComp(partId)) + { + var appearance = AddComp(partId); + appearance.OriginalBody = part.Body; + appearance.Color = bodyAppearance.SkinColor; + UpdateAppearance(partId, appearance); + } + return Containers.Insert(partId, container); } diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs index 04f046bf816778..a969fdfac68571 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs @@ -78,7 +78,6 @@ private void OnToolStep(Entity ent, ref SurgeryStepEvent a var compType = reg.Component.GetType(); if (HasComp(args.Part, compType)) continue; - AddComp(args.Part, _compFactory.GetComponent(compType)); } } @@ -414,7 +413,7 @@ private void OnSurgeryTargetStepChosen(Entity ent, ref S var user = args.Actor; if (GetEntity(args.Entity) is not { Valid: true } body || GetEntity(args.Part) is not { Valid: true } targetPart || - !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, out var surgery, out var part, out var step)) + !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, user, out var surgery, out var part, out var step)) { return; } @@ -571,7 +570,6 @@ public bool IsStepComplete(EntityUid body, EntityUid part, EntProtoId step, Enti var ev = new SurgeryStepCompleteCheckEvent(body, part, surgery); RaiseLocalEvent(stepEnt, ref ev); - return !ev.Cancelled; } diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs index 1f9e1748109b5e..9b1dfab1f0c2fc 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs @@ -73,16 +73,20 @@ private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) private void OnTargetDoAfter(Entity ent, ref SurgeryDoAfterEvent args) { + if (!_timing.IsFirstTimePredicted) + return; + if (args.Cancelled || args.Handled || args.Target is not { } target || - !IsSurgeryValid(ent, target, args.Surgery, args.Step, out var surgery, out var part, out var step) || + !IsSurgeryValid(ent, target, args.Surgery, args.Step, args.User, out var surgery, out var part, out var step) || !PreviousStepsComplete(ent, part, surgery, args.Step) || !CanPerformStep(args.User, ent, part, step, false)) { Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery."); return; } + args.Repeat = HasComp(step); var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User), surgery); RaiseLocalEvent(step, ref ev); @@ -184,21 +188,22 @@ private void OnPartPresentConditionValid(Entity surgeryEnt, out EntityUid part, out EntityUid step) + EntityUid user, out Entity surgeryEnt, out EntityUid part, out EntityUid step) { surgeryEnt = default; part = default; step = default; + if (!HasComp(body) || - !IsLyingDown(body) || + !IsLyingDown(body, user) || GetSingleton(surgery) is not { } surgeryEntId || !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) || !surgeryComp.Steps.Contains(stepId) || - GetSingleton(stepId) is not { } stepEnt) + GetSingleton(stepId) is not { } stepEnt + || !HasComp(targetPart) + && !HasComp(targetPart)) return false; - if (!HasComp(targetPart) && !HasComp(targetPart)) - return false; var ev = new SurgeryValidEvent(body, targetPart); if (_timing.IsFirstTimePredicted) @@ -238,7 +243,7 @@ private List GetTools(EntityUid surgeon) return _hands.EnumerateHeld(surgeon).ToList(); } - public bool IsLyingDown(EntityUid entity) + public bool IsLyingDown(EntityUid entity, EntityUid user) { if (_standing.IsDown(entity)) return true; @@ -251,6 +256,8 @@ public bool IsLyingDown(EntityUid entity) return true; } + _popup.PopupEntity(Loc.GetString("surgery-error-laying"), user, user); + return false; } diff --git a/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs index 5ea663c0a4a073..d2d7f8d4620848 100644 --- a/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs +++ b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs @@ -7,4 +7,4 @@ public sealed partial class SurgeryTargetComponent : Component { [DataField] public bool CanOperate = true; -} \ No newline at end of file +} diff --git a/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs b/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs new file mode 100644 index 00000000000000..9d41401d7f8ad9 --- /dev/null +++ b/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Medical.Surgery; + +[Serializable, NetSerializable] +public sealed class SurgeryUiRefreshEvent : EntityEventArgs +{ + public NetEntity Uid { get; } + + public SurgeryUiRefreshEvent(NetEntity uid) + { + Uid = uid; + } +} diff --git a/Resources/Locale/en-US/surgery/surgery-ui.ftl b/Resources/Locale/en-US/surgery/surgery-ui.ftl index 51fd9392c45b47..fd1c45fcb47106 100644 --- a/Resources/Locale/en-US/surgery/surgery-ui.ftl +++ b/Resources/Locale/en-US/surgery/surgery-ui.ftl @@ -7,5 +7,5 @@ surgery-ui-window-steps-error-skills = You have no surgical skills. surgery-ui-window-steps-error-table = You need an operating table for this. surgery-ui-window-steps-error-armor = You need to remove their armor! surgery-ui-window-steps-error-tools = You're missing tools for this surgery. -surgery-ui-window-steps-error-laying = They need to be laying down! +surgery-error-laying = They need to be laying down! surgery-error-self-surgery = You can't perform surgery on yourself! diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 56b8d3f35bc782..5512cbf5ec04e7 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -1012,6 +1012,7 @@ - MedkitRadiation - MedkitCombat - Scalpel + - BoneGel - Retractor - Cautery - Drill @@ -1540,4 +1541,4 @@ - SynthLeftArm - SynthRightArm - SynthLeftHand - - SynthRightHand \ No newline at end of file + - SynthRightHand diff --git a/Resources/Prototypes/Recipes/Lathes/medical.yml b/Resources/Prototypes/Recipes/Lathes/medical.yml index 10658252a4bada..bec71ce3d13e96 100644 --- a/Resources/Prototypes/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/medical.yml @@ -73,7 +73,7 @@ - type: latheRecipe id: BoneGel result: BoneGel - completetime: 10 + completetime: 2 materials: Plastic: 200 Plasma: 200 From 3ca30f871daab7722616ea5dba2eb7de39d4b003 Mon Sep 17 00:00:00 2001 From: gluesniffler Date: Wed, 6 Nov 2024 02:55:09 -0400 Subject: [PATCH 2/9] defelinization almost real --- Content.Client/Body/Systems/BodySystem.cs | 73 +++++++- .../Humanoid/HumanoidAppearanceSystem.cs | 8 +- Content.Client/Medical/Surgery/SurgeryBui.cs | 44 +---- .../Medical/Surgery/SurgerySystem.cs | 9 - Content.Server/Body/Systems/BodySystem.cs | 24 ++- .../Thresholds/Behaviors/GibBehavior.cs | 5 +- .../Medical/Surgery/SurgerySystem.cs | 16 +- .../Body/Part/BodyPartAppearanceComponent.cs | 27 ++- Content.Shared/Body/Part/BodyPartComponent.cs | 22 ++- Content.Shared/Body/Part/BodyPartEvents.cs | 11 ++ .../Body/Systems/SharedBodySystem.Body.cs | 42 +++-- .../SharedBodySystem.PartAppearance.cs | 175 ++++++++++++------ .../Body/Systems/SharedBodySystem.Parts.cs | 28 ++- .../Body/Systems/SharedBodySystem.cs | 1 + .../Events/ProfileLoadFinishedEvent.cs | 9 + .../SharedHumanoidAppearanceSystem.cs | 2 + .../Surgery/SharedSurgerySystem.Steps.cs | 16 +- Content.Shared/Medical/Surgery/SurgeryUI.cs | 7 +- Resources/Locale/en-US/surgery/surgery-ui.ftl | 2 +- Resources/Prototypes/Body/Parts/harpy.yml | 47 +++++ Resources/Prototypes/Body/Parts/skeleton.yml | 29 +++ .../Prototypes/Entities/Mobs/NPCs/animals.yml | 3 +- .../Entities/Mobs/Player/silicon_base.yml | 2 +- .../Entities/Mobs/Species/skeleton.yml | 5 +- .../Prototypes/Entities/Surgery/surgeries.yml | 4 +- 25 files changed, 440 insertions(+), 171 deletions(-) create mode 100644 Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs index 52af9d93493fd4..bee569c1011fa6 100644 --- a/Content.Client/Body/Systems/BodySystem.cs +++ b/Content.Client/Body/Systems/BodySystem.cs @@ -1,20 +1,83 @@ using Content.Shared.Body.Systems; using Content.Shared.Body.Part; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; using Robust.Client.GameObjects; +using Robust.Shared.Utility; +using Content.Shared.Body.Components; namespace Content.Client.Body.Systems; public sealed class BodySystem : SharedBodySystem { - protected override void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component) + [Dependency] private readonly MarkingManager _markingManager = default!; + + private void ApplyMarkingToPart(MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + SpriteComponent sprite) + { + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + var markingSprite = markingPrototype.Sprites[j]; + + if (markingSprite is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingSprite, j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible) + { + continue; + } + + // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid + // and we need to check the index is correct. + // So if that happens just default to white? + if (colors != null && j < colors.Count) + { + sprite.LayerSetColor(layerId, colors[j]); + } + else + { + sprite.LayerSetColor(layerId, Color.White); + } + } + } + + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) { - if (TryComp(uid, out SpriteComponent? sprite)) + if (!TryComp(target, out SpriteComponent? sprite)) + return; + + if (component.Color != null) + sprite.Color = component.Color.Value; + + foreach (var (visualLayer, markingList) in component.Markings) { - if (component.Color != null) + foreach (var marking in markingList) { - //TODO a few things need to be adjusted before this is ready to be used - also need to find a way to update the player sprite - //sprite.Color = component.Color.Value; + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + continue; + + ApplyMarkingToPart(markingPrototype, marking.MarkingColors, marking.Visible, sprite); } } } + + protected override void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + return; + } } diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 867dcbc2692106..b05a16b6d4f920 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -65,7 +65,8 @@ private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent foreach (var (key, info) in component.CustomBaseLayers) { oldLayers.Remove(key); - SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color); + // Shitmed modification: For whatever reason these weren't actually ignoring the skin color as advertised. + SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color, overrideSkin: true); } // hide old layers @@ -83,7 +84,8 @@ private void SetLayerData( HumanoidVisualLayers key, string? protoId, bool sexMorph = false, - Color? color = null) + Color? color = null, + bool overrideSkin = false) { var layerIndex = sprite.LayerMapReserveBlank(key); var layer = sprite[layerIndex]; @@ -101,7 +103,7 @@ private void SetLayerData( var proto = _prototypeManager.Index(protoId); component.BaseLayers[key] = proto; - if (proto.MatchSkin) + if (proto.MatchSkin && !overrideSkin) layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha); if (proto.BaseSprite != null) diff --git a/Content.Client/Medical/Surgery/SurgeryBui.cs b/Content.Client/Medical/Surgery/SurgeryBui.cs index 5cebffb0914704..b084c8c731aca6 100644 --- a/Content.Client/Medical/Surgery/SurgeryBui.cs +++ b/Content.Client/Medical/Surgery/SurgeryBui.cs @@ -3,8 +3,6 @@ using Content.Shared.Medical.Surgery; using Content.Shared.Body.Components; using Content.Shared.Body.Part; -using Content.Shared.Rotation; -using Content.Shared.Standing; using Content.Client.Hands.Systems; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -14,6 +12,7 @@ using Robust.Shared.Timing; using Robust.Client.Timing; using static Robust.Client.UserInterface.Control; +using OpenToolkit.GraphicsLibraryFramework; namespace Content.Client.Medical.Surgery; @@ -27,7 +26,6 @@ public sealed class SurgeryBui : BoundUserInterface [Dependency] private readonly IGameTiming _timing = default!; private readonly SurgerySystem _system; - private readonly HandsSystem _hands; [ViewVariables] private SurgeryWindow? _window; @@ -40,25 +38,15 @@ public sealed class SurgeryBui : BoundUserInterface public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) { _system = _entities.System(); - _hands = _entities.System(); - - _system.OnStep += RefreshUI; - _hands.OnPlayerItemAdded += OnPlayerItemAdded; } - private void OnPlayerItemAdded(string handName, EntityUid item) + protected override void ReceiveMessage(BoundUserInterfaceMessage message) { - if (_throttling.handName.Equals(handName) - && _throttling.item.Equals(item) - && DateTime.UtcNow - _lastRefresh < TimeSpan.FromSeconds(0.2) - || !_timing.IsFirstTimePredicted - || _window == null - || !_window.IsOpen) + if (_window == null) return; - _throttling = (handName, item); - _lastRefresh = DateTime.UtcNow; - RefreshUI(); + if (message is SurgeryBuiRefreshMessage) + RefreshUI(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -72,8 +60,6 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); if (disposing) _window?.Dispose(); - - _system.OnStep -= RefreshUI; } private void Update(SurgeryBuiState state) @@ -316,7 +302,6 @@ private void RefreshUI() { return; } - Logger.Debug($"Running RefreshUI on {Owner}"); var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent); var i = 0; foreach (var child in _window.Steps.Children) @@ -357,26 +342,7 @@ private void RefreshUI() if (_player.LocalEntity is { } player && status == StepStatus.Next && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _)) - { stepButton.ToolTip = popup; - stepButton.Button.Disabled = true; - - switch (reason) - { - case StepInvalidReason.MissingSkills: - stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-skills")}[/color]"); - break; - case StepInvalidReason.NeedsOperatingTable: - stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-table")}[/color]"); - break; - case StepInvalidReason.Armor: - stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-armor")}[/color]"); - break; - case StepInvalidReason.MissingTool: - stepName.AddMarkup($" [color=red]{Loc.GetString("surgery-ui-window-steps-error-tools")}[/color]"); - break; - } - } } var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default; diff --git a/Content.Client/Medical/Surgery/SurgerySystem.cs b/Content.Client/Medical/Surgery/SurgerySystem.cs index 73af1d85b9e855..cbf1aeee4836ae 100644 --- a/Content.Client/Medical/Surgery/SurgerySystem.cs +++ b/Content.Client/Medical/Surgery/SurgerySystem.cs @@ -4,17 +4,8 @@ namespace Content.Client.Medical.Surgery; public sealed class SurgerySystem : SharedSurgerySystem { - public event Action? OnStep; - public override void Initialize() { base.Initialize(); - - SubscribeNetworkEvent(OnRefresh); - } - - private void OnRefresh(SurgeryUiRefreshEvent ev) - { - OnStep?.Invoke(); } } diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 24116b8e3b7b77..6a2538df8e1e0b 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Body.Part; using Content.Shared.Body.Systems; using Content.Shared.Damage; +using Content.Shared.Gibbing.Events; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; @@ -25,7 +26,6 @@ public sealed class BodySystem : SharedBodySystem [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; public override void Initialize() @@ -111,7 +111,9 @@ public override HashSet GibBody( Vector2? splatDirection = null, float splatModifier = 1, Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null) + SoundSpecifier? gibSoundOverride = null, + GibType gib = GibType.Gib, + GibContentsOption contents = GibContentsOption.Drop) { if (!Resolve(bodyId, ref body, logMissing: false) || TerminatingOrDeleted(bodyId) @@ -125,7 +127,8 @@ public override HashSet GibBody( return new HashSet(); var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs, - splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone); + splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone, + gib: gib, contents: contents); var ev = new BeingGibbedEvent(gibs); RaiseLocalEvent(bodyId, ref ev); @@ -166,8 +169,21 @@ public override HashSet GibPart( return gibs; } - protected override void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component) + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) { return; } + + protected override void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + foreach (var (visualLayer, markingList) in partAppearance.Markings) + { + foreach (var marking in markingList) + { + _humanoidSystem.RemoveMarking(target, marking.MarkingId, sync: false, humanoid: bodyAppearance); + } + } + + Dirty(target, bodyAppearance); + } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs index c83fed1906962b..da054e24ac35ac 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs @@ -1,4 +1,5 @@ using Content.Shared.Body.Components; +using Content.Shared.Gibbing.Events; using JetBrains.Annotations; namespace Content.Server.Destructible.Thresholds.Behaviors @@ -7,13 +8,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors [DataDefinition] public sealed partial class GibBehavior : IThresholdBehavior { + [DataField] public GibType GibType = GibType.Gib; + [DataField] public GibContentsOption GibContents = GibContentsOption.Drop; [DataField("recursive")] private bool _recursive = true; public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) { if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body)) { - system.BodySystem.GibBody(owner, _recursive, body); + system.BodySystem.GibBody(owner, _recursive, body, gib: GibType, contents: GibContents); } } } diff --git a/Content.Server/Medical/Surgery/SurgerySystem.cs b/Content.Server/Medical/Surgery/SurgerySystem.cs index 850ce1430a3449..92ea39b718b31a 100644 --- a/Content.Server/Medical/Surgery/SurgerySystem.cs +++ b/Content.Server/Medical/Surgery/SurgerySystem.cs @@ -54,9 +54,7 @@ public override void Initialize() SubscribeLocalEvent(OnStepAffixPartComplete); SubscribeLocalEvent(OnStepScreamComplete); SubscribeLocalEvent(OnStepSpawnComplete); - SubscribeLocalEvent(OnPrototypesReloaded); - LoadPrototypes(); } @@ -84,17 +82,10 @@ protected override void RefreshUI(EntityUid body) /* Reason we do this is because when applying a BUI State, it rolls back the state on the entity temporarily, which just so happens to occur right as we're checking for step completion, so we end up with the UI - not updating at all until you change tools or reopen the window. + not updating at all until you change tools or reopen the window. I love shitcode. */ - - var actors = _ui.GetActors(body, SurgeryUIKey.Key).ToArray(); - if (actors.Length == 0) - return; - - var filter = Filter.Entities(actors); - RaiseNetworkEvent(new SurgeryUiRefreshEvent(GetNetEntity(body)), filter); + _ui.ServerSendUiMessage(body, SurgeryUIKey.Key, new SurgeryBuiRefreshMessage()); } - private void SetDamage(EntityUid body, DamageSpecifier damage, float partMultiplier, EntityUid user, EntityUid part) { @@ -115,6 +106,7 @@ private void OnToolAfterInteract(Entity ent, ref AfterInte if (args.Handled || !args.CanReach || args.Target == null + || !HasComp(args.Target) || !TryComp(args.User, out var surgery) || !surgery.CanOperate || !IsLyingDown(args.Target.Value, args.User)) @@ -122,7 +114,7 @@ private void OnToolAfterInteract(Entity ent, ref AfterInte return; } - if (user == args.Target && !_config.GetCVar(CCVars.CanOperateOnSelf)) + if (user == args.Target && _config.GetCVar(CCVars.CanOperateOnSelf)) { _popup.PopupEntity(Loc.GetString("surgery-error-self-surgery"), user, user); return; diff --git a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs index 5c9aa5ba60a267..53cf40b9b5f6ba 100644 --- a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs +++ b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs @@ -1,26 +1,45 @@ +using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid.Markings; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.GameStates; namespace Content.Shared.Body.Part; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class BodyPartAppearanceComponent : Component { + /// + /// HumanoidVisualLayer type for this body part. + /// + [DataField, AutoNetworkedField] + public HumanoidVisualLayers Type { get; set; } + + /// + /// Relevant markings for this body part that will be applied on attachment. + /// + [DataField, AutoNetworkedField] + public Dictionary> Markings = new(); /// /// ID of this custom base layer. Must be a . /// - [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] public string? ID { get; set; } /// /// Color of this custom base layer. Null implies skin colour if the corresponding is set to match skin. /// - [DataField("color"), AutoNetworkedField] + [DataField, AutoNetworkedField] public Color? Color { get; set; } - [DataField("originalBody"), AutoNetworkedField] + /// + /// Color of this custom base eye layer. Null implies eye colour if the corresponding is set to match skin. + /// + [DataField, AutoNetworkedField] + public Color? EyeColor { get; set; } + + [DataField, AutoNetworkedField] public EntityUid? OriginalBody { get; set; } //TODO add other custom variables such as species and markings - in case someone decides to attach a lizard arm to a human for example diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs index 62b455b511310f..b71907da53e169 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -1,11 +1,14 @@ -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Medical.Surgery.Tools; -using Content.Shared.Body.Components; +using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Medical.Surgery.Tools; using Content.Shared.FixedPoint; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Content.Shared.Humanoid; namespace Content.Shared.Body.Part; @@ -107,6 +110,19 @@ public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent [DataField, AutoNetworkedField] public ItemSlot ItemInsertionSlot = new(); + + /// + /// Current species. Dictates things like body part sprites. + /// + [DataField, AutoNetworkedField] + public string Species { get; set; } = ""; + + /// + /// Do not make a stupid joke do not make a stupid joke do not make a stupid joke. + /// + [DataField, AutoNetworkedField] + public Sex Sex { get; set; } = Sex.Male; + /// /// These are only for VV/Debug do not use these for gameplay/systems /// diff --git a/Content.Shared/Body/Part/BodyPartEvents.cs b/Content.Shared/Body/Part/BodyPartEvents.cs index f4444121e1d4c5..9872b09200254d 100644 --- a/Content.Shared/Body/Part/BodyPartEvents.cs +++ b/Content.Shared/Body/Part/BodyPartEvents.cs @@ -1,11 +1,22 @@ +using Content.Shared.Humanoid; + namespace Content.Shared.Body.Part; [ByRefEvent] public readonly record struct BodyPartAddedEvent(string Slot, Entity Part); +// Kind of a clone of the above for surgical reattachment specifically. +[ByRefEvent] +public readonly record struct BodyPartAttachedEvent(Entity Part); + [ByRefEvent] public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part); +// Kind of a clone of the above for any instances where we call DropPart(), reasoning being that RemovedEvent fires off +// a lot more often than what I'd like due to PVS. +[ByRefEvent] +public readonly record struct BodyPartDroppedEvent(Entity Part); + [ByRefEvent] public readonly record struct BodyPartEnableChangedEvent(bool Enabled); diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 29409c6b23dc7a..ad12544f33bf72 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -11,6 +11,8 @@ using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Events; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Standing; @@ -51,6 +53,7 @@ private void InitializeBody() SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnProfileLoadFinished); } private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -124,10 +127,11 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId); var rootPart = Comp(rootPartUid); rootPart.Body = bodyEntity; + rootPart.OriginalBody = bodyEntity; Dirty(rootPartUid, rootPart); // Setup the rest of the body entities. SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); - MapInitParts(rootPartUid, prototype); + MapInitParts(rootPartUid, rootPart, prototype); } private void OnBodyCanDrag(Entity ent, ref CanDragEvent args) @@ -171,7 +175,7 @@ private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) /// /// Sets up all of the relevant body parts for a particular body entity and root part. /// - private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) + private void MapInitParts(EntityUid rootPartId, BodyPartComponent rootPart, BodyPrototype prototype) { // Start at the root part and traverse the body graph, setting up parts as we go. // Basic BFS pathfind. @@ -210,6 +214,8 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) var childPartComponent = Comp(childPart); var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent); childPartComponent.ParentSlot = partSlot; + childPartComponent.OriginalBody = rootPart.Body; + Dirty(childPart, childPartComponent); var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection)); if (partSlot is null || !Containers.Insert(childPart, cont)) @@ -219,20 +225,6 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) continue; } - if (TryComp(parentPartComponent.Body, out HumanoidAppearanceComponent? bodyAppearance)) - { - var appearance = AddComp(childPart); - appearance.OriginalBody = childPartComponent.OriginalBody; - appearance.Color = bodyAppearance.SkinColor; - - var symmetry = ((BodyPartSymmetry) childPartComponent.Symmetry).ToString(); - if (symmetry == "None") - symmetry = ""; - appearance.ID = "removed" + symmetry + ((BodyPartType) childPartComponent.PartType).ToString(); - - Dirty(childPart, appearance); - } - // Add organs SetupOrgans((childPart, childPartComponent), connectionSlot.Organs); @@ -347,7 +339,9 @@ public virtual HashSet GibBody( Vector2? splatDirection = null, float splatModifier = 1, Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null) + SoundSpecifier? gibSoundOverride = null, + GibType gib = GibType.Gib, + GibContentsOption contents = GibContentsOption.Drop) { var gibs = new HashSet(); @@ -364,7 +358,7 @@ public virtual HashSet GibBody( foreach (var part in parts) { - _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Drop, ref gibs, + _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, gib, contents, ref gibs, playAudio: false, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone); @@ -416,4 +410,16 @@ public virtual HashSet GibPart( _audioSystem.PlayPredicted(gibSoundOverride, Transform(partId).Coordinates, null); return gibs; } + + private void OnProfileLoadFinished(EntityUid uid, BodyComponent component, ProfileLoadFinishedEvent args) + { + if (!TryComp(uid, out var appearance) + || TerminatingOrDeleted(uid)) + return; + + foreach (var part in GetBodyChildren(uid, component)) + { + EnsureComp(part.Id); + } + } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs index 930da554379c1c..dd5c40fe129285 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs @@ -1,83 +1,150 @@ +using System.Linq; using Content.Shared.Body.Components; using Content.Shared.Body.Part; -using Content.Shared.Body.Events; -using Robust.Shared.GameStates; using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid.Markings; namespace Content.Shared.Body.Systems; -// Code shamelessly stolen from MS14. public partial class SharedBodySystem { + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!; private void InitializePartAppearances() { base.Initialize(); - SubscribeLocalEvent(OnPartAppearanceInit); - SubscribeLocalEvent(OnPartAddedToBody); + SubscribeLocalEvent(OnPartAppearanceStartup); + SubscribeLocalEvent(HandleState); + SubscribeLocalEvent(OnPartAttachedToBody); + SubscribeLocalEvent(OnPartDroppedFromBody); } - private static readonly Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> BodyPartVisualLayers - = new Dictionary<(BodyPartType, BodyPartSymmetry), HumanoidVisualLayers> - { - { (BodyPartType.Head,BodyPartSymmetry.None), HumanoidVisualLayers.Head }, - { (BodyPartType.Tail,BodyPartSymmetry.None), HumanoidVisualLayers.Tail }, - { (BodyPartType.Torso,BodyPartSymmetry.None), HumanoidVisualLayers.Chest }, - { (BodyPartType.Arm,BodyPartSymmetry.Right), HumanoidVisualLayers.RArm }, - { (BodyPartType.Arm,BodyPartSymmetry.Left), HumanoidVisualLayers.LArm }, - { (BodyPartType.Hand,BodyPartSymmetry.Right), HumanoidVisualLayers.RHand }, - { (BodyPartType.Hand,BodyPartSymmetry.Left), HumanoidVisualLayers.LHand }, - { (BodyPartType.Leg,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, - { (BodyPartType.Leg,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg }, - { (BodyPartType.Foot,BodyPartSymmetry.Right), HumanoidVisualLayers.RLeg }, - { (BodyPartType.Foot,BodyPartSymmetry.Left), HumanoidVisualLayers.LLeg } - }; - - private void OnPartAppearanceInit(EntityUid uid, BodyPartAppearanceComponent component, ComponentInit args) + private void OnPartAppearanceStartup(EntityUid uid, BodyPartAppearanceComponent component, ComponentStartup args) { - if (TryComp(uid, out BodyPartComponent? part) && part.OriginalBody != null && - TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance)) - { - var customLayers = bodyAppearance.CustomBaseLayers; - var spriteLayers = bodyAppearance.BaseLayers; - var visualLayer = BodyPartVisualLayers[(part.PartType, part.Symmetry)]; + // God this function reeks, it needs some cleanup BADLY. Help is appreciated as always. - component.OriginalBody = part.OriginalBody.Value; + if (!TryComp(uid, out BodyPartComponent? part) + || part.OriginalBody == null + || TerminatingOrDeleted(part.OriginalBody.Value) + || !TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance) + || HumanoidVisualLayersExtension.ToHumanoidLayers(part) is not { } relevantLayer) + return; - if (customLayers.ContainsKey(visualLayer)) - { - component.ID = customLayers[visualLayer].Id; - component.Color = customLayers[visualLayer].Color; - } - else if (spriteLayers.ContainsKey(visualLayer)) + var customLayers = bodyAppearance.CustomBaseLayers; + var spriteLayers = bodyAppearance.BaseLayers; + component.Type = relevantLayer; + component.OriginalBody = part.OriginalBody.Value; + part.Sex = bodyAppearance.Sex; + + // Thanks Rane for the human reskin :) + if (bodyAppearance.Species.ToString() == "Felinid") + part.Species = "Human"; + else + part.Species = bodyAppearance.Species; + + if (customLayers.ContainsKey(component.Type)) + { + component.ID = customLayers[component.Type].Id; + component.Color = customLayers[component.Type].Color; + } + else if (spriteLayers.ContainsKey(component.Type)) + { + component.ID = spriteLayers[component.Type].ID; + component.Color = bodyAppearance.SkinColor; + } + else + { + component.Color = bodyAppearance.SkinColor; + var symmetryPrefix = part.Symmetry switch { - component.ID = spriteLayers[visualLayer].ID; - component.Color = bodyAppearance.SkinColor; - } - else + BodyPartSymmetry.Left => "L", + BodyPartSymmetry.Right => "R", + _ => "" + }; + + var genderSuffix = ""; + + if (part.PartType == BodyPartType.Torso || part.PartType == BodyPartType.Head) + genderSuffix = part.Sex.ToString(); + + component.ID = $"Mob{part.Species}{symmetryPrefix}{part.PartType}{genderSuffix}"; + } + + // I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS + if (part.PartType == BodyPartType.Head) + component.EyeColor = bodyAppearance.EyeColor; + + var markingsByLayer = new Dictionary>(); + + foreach (var layer in HumanoidVisualLayersExtension.Sublayers(relevantLayer)) + { + var category = MarkingCategoriesConversion.FromHumanoidVisualLayers(layer); + if (bodyAppearance.MarkingSet.Markings.TryGetValue(category, out var markingList)) + markingsByLayer[layer] = markingList.Select(m => new Marking(m.MarkingId, m.MarkingColors.ToList())).ToList(); + } + + component.Markings = markingsByLayer; + } + private void HandleState(EntityUid uid, BodyPartAppearanceComponent component, ref AfterAutoHandleStateEvent args) + { + ApplyPartMarkings(uid, component); + } + private void OnPartAttachedToBody(EntityUid uid, BodyComponent component, ref BodyPartAttachedEvent args) + { + if (!TryComp(args.Part, out BodyPartAppearanceComponent? partAppearance) + || !TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + _humanoid.SetBaseLayerId(uid, partAppearance.Type, partAppearance.ID, sync: true, bodyAppearance); + UpdateAppearance(uid, partAppearance); + } + + private void OnPartDroppedFromBody(EntityUid uid, BodyComponent component, ref BodyPartDroppedEvent args) + { + if (TerminatingOrDeleted(uid) + || !TryComp(args.Part, out BodyPartAppearanceComponent? appearance)) + return; + + RemoveAppearance(uid, appearance, args.Part); + } + + protected void UpdateAppearance(EntityUid target, + BodyPartAppearanceComponent component) + { + if (!TryComp(target, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + if (component.EyeColor != null) + bodyAppearance.EyeColor = component.EyeColor.Value; + + if (component.Color != null) + _humanoid.SetBaseLayerColor(target, component.Type, component.Color, true, bodyAppearance); + + _humanoid.SetLayerVisibility(target, component.Type, true, true, bodyAppearance); + foreach (var (visualLayer, markingList) in component.Markings) + { + _humanoid.SetLayerVisibility(target, visualLayer, true, true, bodyAppearance); + foreach (var marking in markingList) { - var symmetry = ((BodyPartSymmetry) part.Symmetry).ToString(); - if (symmetry == "None") - symmetry = ""; - component.ID = "removed" + symmetry + ((BodyPartType) part.PartType).ToString(); - component.Color = bodyAppearance.SkinColor; + _humanoid.AddMarking(target, marking.MarkingId, marking.MarkingColors, false, true, bodyAppearance); } } - Dirty(uid, component); - UpdateAppearance(uid, component); + Dirty(target, bodyAppearance); } - public void OnPartAddedToBody(EntityUid uid, BodyPartAppearanceComponent component, ref BodyPartAddedEvent args) + protected void RemoveAppearance(EntityUid entity, BodyPartAppearanceComponent component, EntityUid partEntity) { - if (TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance)) + if (!TryComp(entity, out HumanoidAppearanceComponent? bodyAppearance)) + return; + + foreach (var (visualLayer, markingList) in component.Markings) { - var part = args.Part; - var customLayers = bodyAppearance.CustomBaseLayers; - var visualLayer = BodyPartVisualLayers[(part.Comp.PartType, part.Comp.Symmetry)]; - customLayers[visualLayer] = new CustomBaseLayerInfo(component.ID, customLayers[visualLayer].Color); + _humanoid.SetLayerVisibility(entity, visualLayer, false, true, bodyAppearance); } + RemovePartMarkings(entity, component, bodyAppearance); } - protected abstract void UpdateAppearance(EntityUid uid, BodyPartAppearanceComponent component); + protected abstract void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component); + + protected abstract void RemovePartMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance); } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index 2e1b39a480908c..77f8f67ebb785d 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -169,14 +169,25 @@ protected virtual void RemovePart( protected virtual void DropPart(Entity partEnt) { ChangeSlotState(partEnt, true); - + // I don't know if this can cause issues, since any part that's being detached HAS to have a Body. + // though I really just want the compiler to shut the fuck up. + var body = partEnt.Comp.Body.GetValueOrDefault(); // We then detach the part, which will kickstart EntRemovedFromContainer events. if (TryComp(partEnt, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted) { - var ev = new BodyPartEnableChangedEvent(false); - RaiseLocalEvent(partEnt, ref ev); + var enableEvent = new BodyPartEnableChangedEvent(false); + RaiseLocalEvent(partEnt, ref enableEvent); + + if (TryComp(body, out HumanoidAppearanceComponent? bodyAppearance) + && !HasComp(partEnt) + && !TerminatingOrDeleted(body) + && !TerminatingOrDeleted(partEnt)) + EnsureComp(partEnt); + SharedTransform.AttachToGridOrMap(partEnt, transform); _randomHelper.RandomOffset(partEnt, 0.5f); + var droppedEvent = new BodyPartDroppedEvent(partEnt); + RaiseLocalEvent(body, ref droppedEvent); } } @@ -605,13 +616,10 @@ public bool AttachPart( part.ParentSlot = slot; if (TryComp(part.Body, out HumanoidAppearanceComponent? bodyAppearance) - && !HasComp(partId)) - { - var appearance = AddComp(partId); - appearance.OriginalBody = part.Body; - appearance.Color = bodyAppearance.SkinColor; - UpdateAppearance(partId, appearance); - } + && !HasComp(partId) + && !TerminatingOrDeleted(parentPartId) + && !TerminatingOrDeleted(partId)) // Saw some exceptions involving these due to the spawn menu. + EnsureComp(partId); return Containers.Insert(partId, container); } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs index 1d9aae69976aff..013e302633b648 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.cs @@ -44,6 +44,7 @@ public override void Initialize() InitializeParts(); // To try and mitigate the server load due to integrity checks, we set up a Job Queue. InitializeIntegrityQueue(); + InitializePartAppearances(); } /// diff --git a/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs new file mode 100644 index 00000000000000..fc894854dcf1d1 --- /dev/null +++ b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Humanoid.Events; + +/// +/// Raised on an entity when their profile has finished being loaded +/// +public sealed class ProfileLoadFinishedEvent : EntityEventArgs +{ +} + diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index a1e8bec2cd80a3..f33c65b59152f1 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Examine; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid.Events; using Content.Shared.IdentityManagement; using Content.Shared.Preferences; using Content.Shared.HeightAdjust; @@ -440,6 +441,7 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned Dirty(humanoid); + RaiseLocalEvent(uid, new ProfileLoadFinishedEvent()); } /// diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs index a969fdfac68571..fed6be01a24ca0 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs @@ -4,7 +4,6 @@ using Content.Shared.Medical.Surgery.Tools; //using Content.Shared._RMC14.Xenonids.Parasite; using Content.Shared.Body.Part; -using Content.Shared.Body.Systems; using Content.Shared.Body.Organ; using Content.Shared.Bed.Sleep; using Content.Shared.Body.Events; @@ -161,6 +160,19 @@ private void OnToolCanPerform(Entity ent, ref SurgeryCanPe } } + if (_inventory.TryGetContainerSlotEnumerator(args.Body, out var containerSlotEnumerator, args.TargetSlots)) + { + while (containerSlotEnumerator.MoveNext(out var containerSlot)) + { + if (!containerSlot.ContainedEntity.HasValue) + continue; + + args.Invalid = StepInvalidReason.Armor; + args.Popup = Loc.GetString("surgery-ui-window-steps-error-armor"); + return; + } + } + RaiseLocalEvent(args.Body, ref args); if (args.Invalid != StepInvalidReason.None) @@ -301,6 +313,8 @@ private void OnAddPartStep(Entity ent, ref SurgeryS _body.TryCreatePartSlot(args.Part, slotName, partComp.PartType, out var _); _body.AttachPart(args.Part, slotName, tool); _body.ChangeSlotState((tool, partComp), false); + var ev = new BodyPartAttachedEvent((tool, partComp)); + RaiseLocalEvent(args.Body, ref ev); } } } diff --git a/Content.Shared/Medical/Surgery/SurgeryUI.cs b/Content.Shared/Medical/Surgery/SurgeryUI.cs index cffa9cdc4954a1..2572aaca65a72e 100644 --- a/Content.Shared/Medical/Surgery/SurgeryUI.cs +++ b/Content.Shared/Medical/Surgery/SurgeryUI.cs @@ -15,6 +15,11 @@ public sealed class SurgeryBuiState(Dictionary> choi public readonly Dictionary> Choices = choices; } +[Serializable, NetSerializable] +public sealed class SurgeryBuiRefreshMessage : BoundUserInterfaceMessage +{ +} + [Serializable, NetSerializable] public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, EntProtoId step, bool isBody) : BoundUserInterfaceMessage { @@ -24,4 +29,4 @@ public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, // Used as a marker for whether or not we're hijacking surgery by applying it on the body itself. public readonly bool IsBody = isBody; -} \ No newline at end of file +} diff --git a/Resources/Locale/en-US/surgery/surgery-ui.ftl b/Resources/Locale/en-US/surgery/surgery-ui.ftl index fd1c45fcb47106..cf58da3977be55 100644 --- a/Resources/Locale/en-US/surgery/surgery-ui.ftl +++ b/Resources/Locale/en-US/surgery/surgery-ui.ftl @@ -6,6 +6,6 @@ surgery-ui-window-steps = < Steps surgery-ui-window-steps-error-skills = You have no surgical skills. surgery-ui-window-steps-error-table = You need an operating table for this. surgery-ui-window-steps-error-armor = You need to remove their armor! -surgery-ui-window-steps-error-tools = You're missing tools for this surgery. +surgery-ui-window-steps-error-tools = Missing tools. surgery-error-laying = They need to be laying down! surgery-error-self-surgery = You can't perform surgery on yourself! diff --git a/Resources/Prototypes/Body/Parts/harpy.yml b/Resources/Prototypes/Body/Parts/harpy.yml index 9e51334406fa04..8b463f953fc326 100644 --- a/Resources/Prototypes/Body/Parts/harpy.yml +++ b/Resources/Prototypes/Body/Parts/harpy.yml @@ -6,6 +6,8 @@ components: - type: Damageable damageContainer: Biological + - type: Gibbable + - type: SurgeryTool - type: BodyPart - type: ContainerContainer containers: @@ -16,6 +18,36 @@ - type: Tag tags: - Trash + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Blunt + damage: 50 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Slash + damage: 100 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 200 + behaviors: + - !type:SpawnEntitiesBehavior + spawnInContainer: true + spawn: + Ash: + min: 1 + max: 1 + - !type:BurnBodyBehavior { } + - !type:PlaySoundBehavior + sound: + collection: MeatLaserImpact + - type: entity id: TorsoHarpy @@ -31,6 +63,11 @@ state: "torso_m" - type: BodyPart partType: Torso + toolName: "a torso" + containerName: "torso_slot" + - type: ContainerContainer + containers: + torso_slot: !type:ContainerSlot {} - type: entity id: HeadHarpy @@ -46,6 +83,7 @@ state: "head_m" - type: BodyPart partType: Head + toolName: "a head" vital: true - type: Input context: "ghost" @@ -70,6 +108,7 @@ - type: BodyPart partType: Arm symmetry: Left + toolName: "a left arm" - type: entity id: RightArmHarpy @@ -86,6 +125,7 @@ - type: BodyPart partType: Arm symmetry: Right + toolName: "a right arm" - type: entity id: LeftHandHarpy @@ -102,6 +142,7 @@ - type: BodyPart partType: Hand symmetry: Left + toolName: "a left hand" - type: entity id: RightHandHarpy @@ -118,6 +159,7 @@ - type: BodyPart partType: Hand symmetry: Right + toolName: "a right hand" - type: entity id: LeftLegHarpy @@ -134,6 +176,7 @@ - type: BodyPart partType: Leg symmetry: Left + toolName: "a left leg" - type: MovementBodyPart - type: entity @@ -151,6 +194,7 @@ - type: BodyPart partType: Leg symmetry: Right + toolName: "a right leg" - type: MovementBodyPart - type: entity @@ -168,6 +212,7 @@ - type: BodyPart partType: Foot symmetry: Left + toolName: "a left foot" - type: entity id: RightFootHarpy @@ -184,3 +229,5 @@ - type: BodyPart partType: Foot symmetry: Right + toolName: "a right foot" + diff --git a/Resources/Prototypes/Body/Parts/skeleton.yml b/Resources/Prototypes/Body/Parts/skeleton.yml index ffba0c7c44aa85..e2200f7194eeb2 100644 --- a/Resources/Prototypes/Body/Parts/skeleton.yml +++ b/Resources/Prototypes/Body/Parts/skeleton.yml @@ -18,6 +18,35 @@ - type: Tag tags: - Trash + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Blunt + damage: 50 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Slash + damage: 100 + behaviors: + - !type:GibPartBehavior { } + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 200 + behaviors: + - !type:SpawnEntitiesBehavior + spawnInContainer: true + spawn: + Ash: + min: 1 + max: 1 + - !type:BurnBodyBehavior { } + - !type:PlaySoundBehavior + sound: + collection: MeatLaserImpact - type: entity id: TorsoSkeleton diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index cc43c4e0d400e2..4e9326f00cea35 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -397,7 +397,8 @@ damageType: Blunt damage: 10 behaviors: - - !type:GibBehavior { } + - !type:GibBehavior + gibContents: Skip - type: NonSpreaderZombie - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml index a3879c98ce119d..003e7953cefbcc 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon_base.yml @@ -79,7 +79,7 @@ - trigger: !type:DamageTrigger damage: 500 behaviors: - - !type:GibBehavior {} + - !type:GibBehavior { } - type: Icon sprite: Mobs/Species/IPC/parts.rsi state: full diff --git a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml index 28ea5b030f6a57..308e4400e2b6c4 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml @@ -9,7 +9,7 @@ components: - type: HumanoidAppearance species: Skeleton - - type: Carriable # Carrying system from nyanotrasen. + - type: Carriable # Carrying system from nyanotrasen. - type: Icon sprite: Mobs/Species/Skeleton/parts.rsi state: full @@ -40,7 +40,8 @@ !type:DamageTrigger damage: 150 behaviors: - - !type:GibBehavior { } + - !type:GibBehavior + gibContents: Skip - type: SlowOnDamage #modified speeds because they're so weak speedModifierThresholds: 60: 0.9 diff --git a/Resources/Prototypes/Entities/Surgery/surgeries.yml b/Resources/Prototypes/Entities/Surgery/surgeries.yml index 1729cdd306c9d8..5b0e7c83abf8bc 100644 --- a/Resources/Prototypes/Entities/Surgery/surgeries.yml +++ b/Resources/Prototypes/Entities/Surgery/surgeries.yml @@ -69,7 +69,7 @@ categories: [ HideSpawnMenu ] components: - type: Surgery - requirement: SurgeryOpenIncision + #requirement: SurgeryOpenIncision steps: - SurgeryStepInsertFeature - SurgeryStepSealWounds @@ -149,7 +149,7 @@ - type: entity parent: SurgeryBase id: SurgeryAttachLeftHand - name: Attach Left Han + name: Attach Left Hand categories: [ HideSpawnMenu ] components: - type: Surgery From 1d38934899a7ea521637cafea6329655e0a3432d Mon Sep 17 00:00:00 2001 From: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:00:55 +0200 Subject: [PATCH 3/9] Merge pull request #169 from XavierSomething/HealthAnalyzerUIWizden Updated Health Analyzer UI From Latest Wizden Update --- .../UI/HealthAnalyzerWindow.xaml | 68 ++++--- .../UI/HealthAnalyzerWindow.xaml.cs | 168 +++++++++--------- .../components/health-analyzer-component.ftl | 51 +++++- 3 files changed, 172 insertions(+), 115 deletions(-) diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index 401f9768629390..a096caa4cfd977 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -1,48 +1,64 @@  + MaxHeight="525" + MinWidth="300"> diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index fcf6d4551fdd64..1ad748bf7af50a 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -1,12 +1,20 @@ using System.Linq; using System.Numerics; +using Content.Client.Message; using Content.Shared.Atmos; using Content.Client.UserInterface.Controls; +using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.IdentityManagement; +using Content.Shared.Inventory; using Content.Shared.MedicalScanner; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; @@ -28,9 +36,6 @@ public sealed partial class HealthAnalyzerWindow : FancyWindow private readonly IPrototypeManager _prototypes; private readonly IResourceCache _cache; - private const int AnalyzerHeight = 430; - private const int AnalyzerWidth = 300; - public HealthAnalyzerWindow() { RobustXamlLoader.Load(this); @@ -44,8 +49,6 @@ public HealthAnalyzerWindow() public void Populate(HealthAnalyzerScannedUserMessage msg) { - GroupsContainer.RemoveAllChildren(); - var target = _entityManager.GetEntity(msg.TargetEntity); if (target == null @@ -57,82 +60,96 @@ public void Populate(HealthAnalyzerScannedUserMessage msg) NoPatientDataText.Visible = false; - string entityName = Loc.GetString("health-analyzer-window-entity-unknown-text"); - if (_entityManager.HasComponent(target.Value)) - { - entityName = Identity.Name(target.Value, _entityManager); - } + // Scan Mode - if (msg.ScanMode.HasValue) - { - ScanModePanel.Visible = true; - ScanModeText.Text = Loc.GetString(msg.ScanMode.Value ? "health-analyzer-window-scan-mode-active" : "health-analyzer-window-scan-mode-inactive"); - ScanModeText.FontColorOverride = msg.ScanMode.Value ? Color.Green : Color.Red; - } - else - { - ScanModePanel.Visible = false; - } + ScanModeLabel.Text = msg.ScanMode.HasValue + ? msg.ScanMode.Value + ? Loc.GetString("health-analyzer-window-scan-mode-active") + : Loc.GetString("health-analyzer-window-scan-mode-inactive") + : Loc.GetString("health-analyzer-window-entity-unknown-text"); + + ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red; + + // Patient Information + + SpriteView.SetEntity(target.Value); + + var name = new FormattedMessage(); + name.PushColor(Color.White); + name.AddText(_entityManager.HasComponent(target.Value) + ? Identity.Name(target.Value, _entityManager) + : Loc.GetString("health-analyzer-window-entity-unknown-text")); + NameLabel.SetMessage(name); + + SpeciesLabel.Text = + _entityManager.TryGetComponent(target.Value, + out var humanoidAppearanceComponent) + ? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name) + : Loc.GetString("health-analyzer-window-entity-unknown-species-text"); + + // Basic Diagnostic + + TemperatureLabel.Text = !float.IsNaN(msg.Temperature) + ? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)" + : Loc.GetString("health-analyzer-window-entity-unknown-value-text"); + + BloodLabel.Text = !float.IsNaN(msg.BloodLevel) + ? $"{msg.BloodLevel * 100:F1} %" + : Loc.GetString("health-analyzer-window-entity-unknown-value-text"); - PatientName.Text = Loc.GetString( - "health-analyzer-window-entity-health-text", - ("entityName", entityName) - ); + StatusLabel.Text = + _entityManager.TryGetComponent(target.Value, out var mobStateComponent) + ? GetStatus(mobStateComponent.CurrentState) + : Loc.GetString("health-analyzer-window-entity-unknown-text"); - Temperature.Text = Loc.GetString("health-analyzer-window-entity-temperature-text", - ("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)") - ); + // Total Damage - BloodLevel.Text = Loc.GetString("health-analyzer-window-entity-blood-level-text", - ("bloodLevel", float.IsNaN(msg.BloodLevel) ? "N/A" : $"{msg.BloodLevel * 100:F1} %") - ); + DamageLabel.Text = damageable.TotalDamage.ToString(); + + // Alerts + + AlertsDivider.Visible = msg.Bleeding == true; + AlertsContainer.Visible = msg.Bleeding == true; if (msg.Bleeding == true) { - Bleeding.Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"); - Bleeding.FontColorOverride = Color.Red; - } - else - { - Bleeding.Text = string.Empty; // Clear the text + AlertsContainer.DisposeAllChildren(); + AlertsContainer.AddChild(new Label + { + Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"), + FontColorOverride = Color.Red, + }); } - patientDamageAmount.Text = Loc.GetString( - "health-analyzer-window-entity-damage-total-text", - ("amount", damageable.TotalDamage) - ); + // Damage Groups var damageSortedGroups = - damageable.DamagePerGroup.OrderBy(damage => damage.Value) + damageable.DamagePerGroup.OrderByDescending(damage => damage.Value) .ToDictionary(x => x.Key, x => x.Value); + IReadOnlyDictionary damagePerType = damageable.Damage.DamageDict; DrawDiagnosticGroups(damageSortedGroups, damagePerType); + } - if (_entityManager.TryGetComponent(target, out HungerComponent? hunger) - && hunger.StarvationDamage != null - && hunger.CurrentThreshold <= HungerThreshold.Starving) + private static string GetStatus(MobState mobState) + { + return mobState switch { - var box = new Control { Margin = new Thickness(0, 0, 0, 15) }; - - box.AddChild(CreateDiagnosticGroupTitle( - Loc.GetString("health-analyzer-window-malnutrition"), - "malnutrition")); - - GroupsContainer.AddChild(box); - } - - SetHeight = AnalyzerHeight; - SetWidth = AnalyzerWidth; + MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"), + MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"), + MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"), + _ => Loc.GetString("health-analyzer-window-entity-unknown-text"), + }; } private void DrawDiagnosticGroups( - Dictionary groups, IReadOnlyDictionary damageDict) + Dictionary groups, + IReadOnlyDictionary damageDict) { - HashSet shownTypes = new(); + GroupsContainer.RemoveAllChildren(); - // Show the total damage and type breakdown for each damage group. - foreach (var (damageGroupId, damageAmount) in groups.Reverse()) + foreach (var (damageGroupId, damageAmount) in groups) { if (damageAmount == 0) continue; @@ -145,7 +162,6 @@ private void DrawDiagnosticGroups( var groupContainer = new BoxContainer { - Margin = new Thickness(0, 0, 0, 15), Align = BoxContainer.AlignMode.Begin, Orientation = BoxContainer.LayoutOrientation.Vertical, }; @@ -159,23 +175,16 @@ private void DrawDiagnosticGroups( foreach (var type in group.DamageTypes) { - if (damageDict.TryGetValue(type, out var typeAmount) && typeAmount > 0) - { - // If damage types are allowed to belong to more than one damage group, - // they may appear twice here. Mark them as duplicate. - if (shownTypes.Contains(type)) - continue; - - shownTypes.Add(type); - - var damageString = Loc.GetString( - "health-analyzer-window-damage-type-text", - ("damageType", _prototypes.Index(type).LocalizedName), - ("amount", typeAmount) - ); - - groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, "- "))); - } + if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0) + continue; + + var damageString = Loc.GetString( + "health-analyzer-window-damage-type-text", + ("damageType", Loc.GetString("health-analyzer-window-damage-type-" + type)), + ("amount", typeAmount) + ); + + groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · "))); } } } @@ -198,7 +207,6 @@ private static Label CreateDiagnosticItemLabel(string text) { return new Label { - Margin = new Thickness(2, 2), Text = text, }; } @@ -207,13 +215,13 @@ private BoxContainer CreateDiagnosticGroupTitle(string text, string id) { var rootContainer = new BoxContainer { + Margin = new Thickness(0, 6, 0, 0), VerticalAlignment = VAlignment.Bottom, - Orientation = BoxContainer.LayoutOrientation.Horizontal + Orientation = BoxContainer.LayoutOrientation.Horizontal, }; rootContainer.AddChild(new TextureRect { - Margin = new Thickness(0, 3), SetSize = new Vector2(30, 30), Texture = GetTexture(id.ToLower()) }); diff --git a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl index 8460bcc27b0626..d2ad240daa60d3 100644 --- a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl +++ b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl @@ -1,16 +1,49 @@ health-analyzer-window-no-patient-data-text = No patient data. -health-analyzer-window-entity-unknown-text = unknown -health-analyzer-window-entity-health-text = {$entityName}'s health: -health-analyzer-window-entity-temperature-text = Temperature: {$temperature} -health-analyzer-window-entity-blood-level-text = Blood Level: {$bloodLevel} -health-analyzer-window-entity-bleeding-text = Patient is bleeding! -health-analyzer-window-entity-damage-total-text = Total Damage: {$amount} +health-analyzer-window-entity-unknown-text = Unknown +health-analyzer-window-entity-unknown-species-text = Non-Humanoid +health-analyzer-window-entity-unknown-value-text = N/A + +health-analyzer-window-entity-alive-text = Alive +health-analyzer-window-entity-dead-text = Dead +health-analyzer-window-entity-critical-text = Critical + +health-analyzer-window-entity-temperature-text = Temperature: +health-analyzer-window-entity-blood-level-text = Blood Level: +health-analyzer-window-entity-status-text = Status: +health-analyzer-window-entity-damage-total-text = Total Damage: + health-analyzer-window-damage-group-text = {$damageGroup}: {$amount} health-analyzer-window-damage-type-text = {$damageType}: {$amount} health-analyzer-window-damage-type-duplicate-text = {$damageType}: {$amount} (duplicate) -health-analyzer-window-scan-mode-text = Scan Mode: -health-analyzer-window-scan-mode-active = ACTIVE -health-analyzer-window-scan-mode-inactive = INACTIVE +health-analyzer-window-damage-group-Brute = Brute +health-analyzer-window-damage-type-Blunt = Blunt +health-analyzer-window-damage-type-Slash = Slash +health-analyzer-window-damage-type-Piercing = Piercing + +health-analyzer-window-damage-group-Burn = Burn +health-analyzer-window-damage-type-Heat = Heat +health-analyzer-window-damage-type-Shock = Shock +health-analyzer-window-damage-type-Cold = Cold +health-analyzer-window-damage-type-Caustic = Caustic + +health-analyzer-window-damage-group-Airloss = Airloss +health-analyzer-window-damage-type-Asphyxiation = Asphyxiation +health-analyzer-window-damage-type-Bloodloss = Bloodloss + +health-analyzer-window-damage-group-Toxin = Toxin +health-analyzer-window-damage-type-Poison = Poison +health-analyzer-window-damage-type-Radiation = Radiation + +health-analyzer-window-damage-group-Genetic = Genetic +health-analyzer-window-damage-type-Cellular = Cellular health-analyzer-window-malnutrition = Severely malnourished + +health-analyzer-window-entity-bleeding-text = Patient is bleeding! + +health-analyzer-window-scan-mode-text = Scan Mode: +health-analyzer-window-scan-mode-active = Active +health-analyzer-window-scan-mode-inactive = Inactive + +health-analyzer-popup-scan-target = {CAPITALIZE(THE($user))} is trying to scan you! From f3af74f9416508d68955a045e2e1817453a11782 Mon Sep 17 00:00:00 2001 From: goet <6637097+goet@users.noreply.github.com> Date: Sat, 21 Sep 2024 07:54:48 +0200 Subject: [PATCH 4/9] Fix medical PDA/health analyzer long range intel bug (#31879) * hide spriteview from health analyzer while inactive * add out of range indicator if analyzer becomes inactive * hide out of range icon if there is no patient data --- .../HealthAnalyzer/UI/HealthAnalyzerWindow.xaml | 1 + .../UI/HealthAnalyzerWindow.xaml.cs | 2 ++ .../Misc/health_analyzer_out_of_range.png | Bin 0 -> 224 bytes 3 files changed, 3 insertions(+) create mode 100644 Resources/Textures/Interface/Misc/health_analyzer_out_of_range.png diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index a096caa4cfd977..892d033adc8a1c 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -21,6 +21,7 @@ Orientation="Vertical"> +