using System.Threading; using Content.Server.Body.Components; using Content.Server.Cloning; using Content.Server.DoAfter; using Content.Server.Mind.Components; using Content.Server.Popups; using Content.Server.Preferences.Managers; using Content.Shared.Actions; using Content.Shared.CharacterAppearance.Systems; using Content.Shared.IdentityManagement; using Content.Shared.Preferences; using Content.Shared.Species; using Content.Shared.Verbs; using Robust.Shared.Player; using Robust.Shared.Prototypes; /// /// Fair warning, this is all kinda shitcode, but it'll have to wait for a major /// refactor until proper body systems get added. The current implementation is /// definitely not ideal and probably will be prone to weird bugs. /// namespace Content.Server.Body.Systems { public sealed class BodyReassembleSystem : EntitySystem { [Dependency] private readonly IServerPreferencesManager _prefsManager = null!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!; private const float SelfReassembleMultiplier = 2f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnPartGibbed); SubscribeLocalEvent(StartReassemblyAction); SubscribeLocalEvent>(AddReassembleVerbs); SubscribeLocalEvent(ReassembleComplete); SubscribeLocalEvent(ReassembleCancelled); } private void StartReassemblyAction(EntityUid uid, BodyReassembleComponent component, ReassembleActionEvent args) { args.Handled = true; StartReassembly(uid, component, SelfReassembleMultiplier); } private void ReassembleCancelled(EntityUid uid, BodyReassembleComponent component, ReassembleCancelledEvent args) { component.CancelToken = null; } private void OnPartGibbed(EntityUid uid, BodyReassembleComponent component, PartGibbedEvent args) { if (!TryComp(args.EntityToGib, out var mindComp) || mindComp?.Mind == null) return; component.BodyParts = args.GibbedParts; UpdateDNAEntry(uid, args.EntityToGib); mindComp.Mind.TransferTo(uid); if (component.ReassembleAction == null) return; _actions.AddAction(uid, component.ReassembleAction, null); } private void StartReassembly(EntityUid uid, BodyReassembleComponent component, float multiplier = 1f) { if (component.CancelToken != null) return; if (!GetNearbyParts(uid, component, out var partList)) return; if (partList == null) return; var doAfterTime = component.DoAfterTime * multiplier; var cancelToken = new CancellationTokenSource(); component.CancelToken = cancelToken; var doAfterEventArgs = new DoAfterEventArgs(component.Owner, doAfterTime, cancelToken.Token, component.Owner) { BreakOnTargetMove = true, BreakOnUserMove = true, BreakOnDamage = true, BreakOnStun = true, NeedHand = false, TargetCancelledEvent = new ReassembleCancelledEvent(), TargetFinishedEvent = new ReassembleCompleteEvent(uid, uid, partList), }; _doAfterSystem.DoAfter(doAfterEventArgs); } /// /// Adds the custom verb for reassembling body parts /// private void AddReassembleVerbs(EntityUid uid, BodyReassembleComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract) return; if (!TryComp(uid, out var mind) || !mind.HasMind || component.CancelToken != null) return; // doubles the time if you reconstruct yourself var multiplier = args.User == uid ? SelfReassembleMultiplier : 1f; // Custom verb AlternativeVerb custom = new() { Text = Loc.GetString("reassemble-action"), Act = () => { StartReassembly(uid, component, multiplier); }, IconEntity = uid, Priority = 1 }; args.Verbs.Add(custom); } private bool GetNearbyParts(EntityUid uid, BodyReassembleComponent component, out HashSet? partList) { partList = new HashSet(); if (component.BodyParts == null) return false; // Ensures all of the old body part pieces are there var xformQuery = GetEntityQuery(); var bodyXform = xformQuery.GetComponent(uid); foreach (var part in component.BodyParts) { if (!xformQuery.TryGetComponent(part, out var xform) || !bodyXform.Coordinates.InRange(EntityManager, xform.Coordinates,2f)) { _popupSystem.PopupEntity(Loc.GetString("reassemble-fail"), uid, Filter.Entities(uid)); return false; } partList.Add(part); } return true; } private void ReassembleComplete(EntityUid uid, BodyReassembleComponent component, ReassembleCompleteEvent args) { component.CancelToken = null; if (component.DNA == null || component.BodyParts == null) return; // Creates the new entity and transfers the mind component var speciesProto = _prototype.Index(component.DNA.Value.Profile.Species).Prototype; var mob = EntityManager.SpawnEntity(speciesProto, EntityManager.GetComponent(component.Owner).MapPosition); _humanoidAppearance.UpdateFromProfile(mob, component.DNA.Value.Profile); MetaData(mob).EntityName = component.DNA.Value.Profile.Name; if (TryComp(uid, out var mindcomp) && mindcomp.Mind != null) mindcomp.Mind.TransferTo(mob); // Cleans up all the body part pieces foreach (var entity in component.BodyParts) { EntityManager.DeleteEntity(entity); } _popupSystem.PopupEntity(Loc.GetString("reassemble-success", ("user", Identity.Entity(mob, EntityManager))), mob, Filter.Entities(mob)); } /// /// Called before the skeleton entity is gibbed in order to save /// the dna for reassembly later /// /// the entity that the player will transfer to /// the entity whose DNA is being saved private void UpdateDNAEntry(EntityUid uid, EntityUid body) { if (!TryComp(uid, out var skelBodyComp) || !TryComp(body, out var mindcomp)) return; if (mindcomp.Mind == null) return; if (mindcomp.Mind.UserId == null) return; var profile = (HumanoidCharacterProfile) _prefsManager.GetPreferences(mindcomp.Mind.UserId.Value).SelectedCharacter; skelBodyComp.DNA = new ClonerDNAEntry(mindcomp.Mind, profile); } private sealed class ReassembleCompleteEvent : EntityEventArgs { /// /// The entity being reassembled /// public readonly EntityUid Uid; /// /// The user performing the reassembly /// public readonly EntityUid User; public readonly HashSet PartList; public ReassembleCompleteEvent(EntityUid uid, EntityUid user, HashSet partList) { Uid = uid; User = user; PartList = partList; } } private sealed class ReassembleCancelledEvent : EntityEventArgs {} } } public sealed class ReassembleActionEvent : InstantActionEvent { }