// SPDX-FileCopyrightText: 2023 Moony // SPDX-FileCopyrightText: 2023 Pieter-Jan Briers // SPDX-FileCopyrightText: 2023 TemporalOroboros // SPDX-FileCopyrightText: 2023 Visne <39844191+Visne@users.noreply.github.com> // SPDX-FileCopyrightText: 2023 moonheart08 // SPDX-FileCopyrightText: 2024 AJCM-git <60196617+AJCM-git@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Aiden // SPDX-FileCopyrightText: 2024 Alzore <140123969+Blackern5000@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Baa <9057997+Baa14453@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Bakke // SPDX-FileCopyrightText: 2024 Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 CaasGit <87243814+CaasGit@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Cojoke <83733158+Cojoke-dot@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 DrSmugleaf // SPDX-FileCopyrightText: 2024 Ed <96445749+TheShuEd@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Emisse <99158783+Emisse@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 EmoGarbage404 // SPDX-FileCopyrightText: 2024 Eoin Mcloughlin // SPDX-FileCopyrightText: 2024 Errant <35878406+Errant-4@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Flareguy <78941145+Flareguy@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Hrosts <35345601+Hrosts@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Ian // SPDX-FileCopyrightText: 2024 Ilya246 <57039557+Ilya246@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Jake Huxell // SPDX-FileCopyrightText: 2024 Joel Zimmerman // SPDX-FileCopyrightText: 2024 JustCone <141039037+JustCone14@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Kara // SPDX-FileCopyrightText: 2024 Killerqu00 <47712032+Killerqu00@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Kira Bridgeton <161087999+Verbalase@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Kukutis96513 <146854220+Kukutis96513@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Lye <128915833+Lyroth001@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 MerrytheManokit <167581110+MerrytheManokit@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Mervill // SPDX-FileCopyrightText: 2024 Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 MureixloI <132683811+MureixloI@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 NakataRin <45946146+NakataRin@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Nemanja <98561806+EmoGarbage404@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 OrangeMoronage9622 // SPDX-FileCopyrightText: 2024 PJBot // SPDX-FileCopyrightText: 2024 Pieter-Jan Briers // SPDX-FileCopyrightText: 2024 Piras314 // SPDX-FileCopyrightText: 2024 Plykiya <58439124+Plykiya@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Preston Smith <92108534+thetolbean@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Psychpsyo <60073468+Psychpsyo@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Repo <47093363+Titian3@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 RiceMar1244 <138547931+RiceMar1244@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Simon <63975668+Simyon264@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Sirionaut <148076704+Sirionaut@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Stalen <33173619+stalengd@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 TakoDragon <69509841+BackeTako@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Tayrtahn // SPDX-FileCopyrightText: 2024 Thomas <87614336+Aeshus@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Ubaser <134914314+UbaserB@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 Unkn0wn_Gh0st // SPDX-FileCopyrightText: 2024 Vasilis // SPDX-FileCopyrightText: 2024 Vigers Ray <60344369+VigersRay@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 beck-thompson <107373427+beck-thompson@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 deathride58 // SPDX-FileCopyrightText: 2024 deltanedas <39013340+deltanedas@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 deltanedas <@deltanedas:kde.org> // SPDX-FileCopyrightText: 2024 dffdff2423 // SPDX-FileCopyrightText: 2024 eoineoineoin // SPDX-FileCopyrightText: 2024 foboscheshir <156405958+foboscheshir@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 keronshb <54602815+keronshb@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 lzk <124214523+lzk228@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 metalgearsloth // SPDX-FileCopyrightText: 2024 nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 plykiya // SPDX-FileCopyrightText: 2024 saintmuntzer <47153094+saintmuntzer@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 shamp <140359015+shampunj@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 sirionaut // SPDX-FileCopyrightText: 2024 strO0pwafel <153459934+strO0pwafel@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 stroopwafel // SPDX-FileCopyrightText: 2024 themias <89101928+themias@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 to4no_fix <156101927+chavonadelal@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 username <113782077+whateverusername0@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 voidnull000 <18663194+voidnull000@users.noreply.github.com> // SPDX-FileCopyrightText: 2024 whateverusername0 // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 Aidenkrz // SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 Aviu00 // SPDX-FileCopyrightText: 2025 GoobBot // SPDX-FileCopyrightText: 2025 SX_7 // SPDX-FileCopyrightText: 2025 slarticodefast <161409025+slarticodefast@users.noreply.github.com> // // SPDX-License-Identifier: AGPL-3.0-or-later using Content.Server.Actions; using Content.Server.Humanoid; using Content.Server.Inventory; using Content.Server.Mind.Commands; using Content.Server.Polymorph.Components; using Content.Shared._Shitcode.Wizard.BindSoul; using Content.Shared.Actions; using Content.Shared.Buckle; using Content.Shared.Buckle.Components; using Content.Shared.Damage; using Content.Shared.Destructible; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.NameModifier.Components; using Content.Shared.Nutrition; using Content.Shared.Polymorph; using Content.Shared.Popups; using Content.Shared.Random.Helpers; using Content.Shared.Tag; using Robust.Server.Audio; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Polymorph.Systems; public sealed partial class PolymorphSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; // Goobstation [Dependency] private readonly ISerializationManager _serialization = default!; // Goobstation [Dependency] private readonly IComponentFactory _compFact = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly SharedBuckleSystem _buckle = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; [Dependency] private readonly ServerInventorySystem _inventory = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly TagSystem _tag = default!; // goob edit private ISawmill _sawMill = default!; // Goobstation private const string RevertPolymorphId = "ActionRevertPolymorph"; public override void Initialize() { SubscribeLocalEvent(OnComponentStartup); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnPolymorphActionEvent); SubscribeLocalEvent(OnRevertPolymorphActionEvent); SubscribeLocalEvent(OnBeforeFullyEaten); SubscribeLocalEvent(OnBeforeFullySliced); SubscribeLocalEvent(OnDestruction); InitializeMap(); InitializeTrigger(); InitializeCollide(); _sawMill = Logger.GetSawmill("polymorph"); // Goobstation } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { comp.Time += frameTime; if (comp.Configuration.Duration != null && comp.Time >= comp.Configuration.Duration) { Revert((uid, comp)); continue; } if (!TryComp(uid, out var mob)) continue; if (comp.Configuration.RevertOnDeath && _mobState.IsDead(uid, mob) || comp.Configuration.RevertOnCrit && _mobState.IsIncapacitated(uid, mob)) { Revert((uid, comp)); } } UpdateTrigger(); UpdateCollide(); } private void OnComponentStartup(Entity ent, ref ComponentStartup args) { if (ent.Comp.InnatePolymorphs != null) { foreach (var morph in ent.Comp.InnatePolymorphs) { CreatePolymorphAction(morph, ent); } } } private void OnMapInit(Entity ent, ref MapInitEvent args) { var (uid, component) = ent; if (component.Configuration.Forced) return; if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId)) { action.EntityIcon = component.Parent; action.UseDelay = TimeSpan.FromSeconds(component.Configuration.Delay); } } private void OnPolymorphActionEvent(Entity ent, ref PolymorphActionEvent args) { if (!_proto.TryIndex(args.ProtoId, out var prototype) || args.Handled) return; PolymorphEntity(ent, prototype.Configuration); args.Handled = true; } private void OnRevertPolymorphActionEvent(Entity ent, ref RevertPolymorphActionEvent args) { Revert((ent, ent)); } private void OnBeforeFullyEaten(Entity ent, ref BeforeFullyEatenEvent args) { var (_, comp) = ent; if (comp.Configuration.RevertOnEat) { args.Cancel(); Revert((ent, ent)); } } private void OnBeforeFullySliced(Entity ent, ref BeforeFullySlicedEvent args) { var (_, comp) = ent; if (comp.Configuration.RevertOnEat) { args.Cancel(); Revert((ent, ent)); } } /// /// It is possible to be polymorphed into an entity that can't "die", but is instead /// destroyed. This handler ensures that destruction is treated like death. /// private void OnDestruction(Entity ent, ref DestructionEventArgs args) { if (ent.Comp.Configuration.RevertOnDeath) { Revert((ent, ent)); } } /// /// Polymorphs the target entity into the specific polymorph prototype /// /// The entity that will be transformed /// The id of the polymorph prototype public EntityUid? PolymorphEntity(EntityUid uid, ProtoId protoId) { var config = _proto.Index(protoId).Configuration; return PolymorphEntity(uid, config); } /// /// Polymorphs the target entity into another /// /// The entity that will be transformed /// Polymorph data /// public EntityUid? PolymorphEntity(EntityUid uid, PolymorphConfiguration configuration) { // if it's already morphed, don't allow it again with this condition active. if (!configuration.AllowRepeatedMorphs && HasComp(uid)) return null; // If this polymorph has a cooldown, check if that amount of time has passed since the // last polymorph ended. if (TryComp(uid, out var polymorphableComponent) && polymorphableComponent.LastPolymorphEnd != null && _gameTiming.CurTime < polymorphableComponent.LastPolymorphEnd + configuration.Cooldown) return null; // mostly just for vehicles if (TryComp(uid, out BuckleComponent? buckle)) // Goob edit _buckle.TryUnbuckle((uid, buckle), uid, true); var targetTransformComp = Transform(uid); if (configuration.PolymorphSound != null) _audio.PlayPvs(configuration.PolymorphSound, targetTransformComp.Coordinates); // Goob edit start var proto = configuration.Entity; if (proto == null) { if (!_proto.TryIndex(configuration.Entities, out var entities) || entities.Weights.Count == 0) { if (!_proto.TryIndex(configuration.Groups, out var groups) || groups.Weights.Count == 0) return null; var weightedEntityRandom = groups.Pick(_random); if (!_proto.TryIndex(weightedEntityRandom, out entities) || entities.Weights.Count == 0) return null; } proto = entities.Pick(_random); } var child = Spawn(proto, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid)); MakeSentientCommand.MakeSentient(child, EntityManager, configuration.AllowMovement); // Goob edit end var polymorphedComp = _compFact.GetComponent(); polymorphedComp.Parent = uid; polymorphedComp.Configuration = configuration; AddComp(child, polymorphedComp); var childXform = Transform(child); _transform.SetLocalRotation(child, targetTransformComp.LocalRotation, childXform); // Goob edit start if (configuration.AttachToGridOrMap) _transform.AttachToGridOrMap(child, childXform); else if (_container.TryGetContainingContainer((uid, targetTransformComp, null), out var cont)) _container.Insert(child, cont); // Goob edit end //Transfers all damage from the original to the new one if (configuration.TransferDamage && TryComp(child, out var damageParent) && _mobThreshold.GetScaledDamage(uid, child, out var damage) && damage != null) { _damageable.SetDamage(child, damageParent, damage); } if (configuration.Inventory == PolymorphInventoryChange.Transfer) { // Goob edit start if (TryComp(uid, out InventoryComponent? inventory1)) { if (TryComp(child, out InventoryComponent? inventory2)) { _inventory.TransferEntityInventories((uid, inventory1), (child, inventory2)); foreach (var hand in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, hand, checkActionBlocker: false); _hands.TryPickupAnyHand(child, hand); } } else { if (_inventory.TryGetContainerSlotEnumerator((uid, inventory1), out var enumerator)) { while (enumerator.MoveNext(out var slot)) { _inventory.TryUnequip(uid, slot.ID, true, true); } } foreach (var held in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, held); } } } // Goob edit end } else if (configuration.Inventory == PolymorphInventoryChange.Drop) { if (_inventory.TryGetContainerSlotEnumerator(uid, out var enumerator)) { while (enumerator.MoveNext(out var slot)) { _inventory.TryUnequip(uid, slot.ID, true, true); } } foreach (var held in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, held); } } if (configuration.TransferName && TryComp(uid, out MetaDataComponent? targetMeta)) { // Goob edit start _metaData.SetEntityName(child, TryComp(uid, out NameModifierComponent? modifier) ? modifier.BaseName : targetMeta.EntityName); // Goob edit end } if (configuration.TransferHumanoidAppearance) { _humanoid.CloneAppearance(uid, child); } if (configuration.ComponentsToTransfer.Count > 0) // Goobstation { foreach (var data in configuration.ComponentsToTransfer) { Type type; try { type = _compFact.GetRegistration(data.Component).Type; } catch (UnknownComponentException e) { _sawMill.Error(e.Message); continue; } if (!EntityManager.TryGetComponent(uid, type, out var component)) continue; var newComp = _compFact.GetComponent(type); if (data.Mirror) { if (!HasComp(child, type)) AddComp(child, newComp); continue; } if (!data.Override && HasComp(child, type)) continue; object? temp = (Component) newComp; _serialization.CopyTo(component, ref temp, notNullableOverride: true); EntityManager.AddComponent(child, (Component) temp!, true); } } _tag.AddTag(uid, SharedBindSoulSystem.IgnoreBindSoulTag); // Goobstation if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, child, mind: mind); _tag.RemoveTag(uid, SharedBindSoulSystem.IgnoreBindSoulTag); // Goobstation //Ensures a map to banish the entity to EnsurePausedMap(); if (PausedMap != null) _transform.SetParent(uid, targetTransformComp, PausedMap.Value); // Raise an event to inform anything that wants to know about the entity swap var ev = new PolymorphedEvent(uid, child, false); RaiseLocalEvent(uid, ref ev); RaiseLocalEvent(child, ref ev); return child; } /// /// Reverts a polymorphed entity back into its original form /// /// The entityuid of the entity being reverted /// public EntityUid? Revert(Entity ent) { var (uid, component) = ent; if (!Resolve(ent, ref component)) return null; if (Deleted(uid)) return null; var parent = component.Parent; if (Deleted(parent)) return null; var uidXform = Transform(uid); var parentXform = Transform(parent); if (component.Configuration.ExitPolymorphSound != null) _audio.PlayPvs(component.Configuration.ExitPolymorphSound, uidXform.Coordinates); _transform.SetParent(parent, parentXform, uidXform.ParentUid); _transform.SetCoordinates(parent, parentXform, uidXform.Coordinates, uidXform.LocalRotation); if (component.Configuration.TransferDamage && TryComp(parent, out var damageParent) && _mobThreshold.GetScaledDamage(uid, parent, out var damage) && damage != null) { _damageable.SetDamage(parent, damageParent, damage); } if (component.Configuration.Inventory == PolymorphInventoryChange.Transfer) { _inventory.TransferEntityInventories(uid, parent); foreach (var held in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, held); _hands.TryPickupAnyHand(parent, held, checkActionBlocker: false); } } else if (component.Configuration.Inventory == PolymorphInventoryChange.Drop) { if (_inventory.TryGetContainerSlotEnumerator(uid, out var enumerator)) { while (enumerator.MoveNext(out var slot)) { _inventory.TryUnequip(uid, slot.ID); } } foreach (var held in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, held); } } _tag.AddTag(uid, SharedBindSoulSystem.IgnoreBindSoulTag); // Goobstation if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, parent, mind: mind); _tag.RemoveTag(uid, SharedBindSoulSystem.IgnoreBindSoulTag); // Goobstation if (TryComp(parent, out var polymorphableComponent)) polymorphableComponent.LastPolymorphEnd = _gameTiming.CurTime; // if an item polymorph was picked up, put it back down after reverting _transform.AttachToGridOrMap(parent, parentXform); // Raise an event to inform anything that wants to know about the entity swap var ev = new PolymorphedEvent(uid, parent, true); RaiseLocalEvent(uid, ref ev); RaiseLocalEvent(parent, ref ev); if (component.Configuration.ShowPopup) // Goob edit { _popup.PopupEntity(Loc.GetString("polymorph-revert-popup-generic", ("parent", Identity.Entity(uid, EntityManager)), ("child", Identity.Entity(parent, EntityManager))), parent); } QueueDel(uid); return parent; } /// /// Creates a sidebar action for an entity to be able to polymorph at will /// /// The string of the id of the polymorph action /// The entity that will be gaining the action public void CreatePolymorphAction(ProtoId id, Entity target) { target.Comp.PolymorphActions ??= new(); if (target.Comp.PolymorphActions.ContainsKey(id)) return; if (!_proto.TryIndex(id, out var polyProto)) return; // Goob edit start if (polyProto.Configuration.Entity == null) return; var entProto = _proto.Index(polyProto.Configuration.Entity.Value); // Goob edit end EntityUid? actionId = default!; if (!_actions.AddAction(target, ref actionId, RevertPolymorphId, target)) return; target.Comp.PolymorphActions.Add(id, actionId.Value); var metaDataCache = MetaData(actionId.Value); _metaData.SetEntityName(actionId.Value, Loc.GetString("polymorph-self-action-name", ("target", entProto.Name)), metaDataCache); _metaData.SetEntityDescription(actionId.Value, Loc.GetString("polymorph-self-action-description", ("target", entProto.Name)), metaDataCache); if (!_actions.TryGetActionData(actionId, out var baseAction)) return; baseAction.Icon = new SpriteSpecifier.EntityPrototype(polyProto.Configuration.Entity.Value); // Goob edit if (baseAction is InstantActionComponent action) action.Event = new PolymorphActionEvent(id); } public void RemovePolymorphAction(ProtoId id, Entity target) { if (target.Comp.PolymorphActions == null) return; if (target.Comp.PolymorphActions.TryGetValue(id, out var val)) _actions.RemoveAction(target, val); } }