using Content.Server.Actions; using Content.Server.Humanoid; using Content.Server.Inventory; using Content.Server.Mind.Commands; using Content.Server.Nutrition; using Content.Server.Polymorph.Components; using Content.Shared.Actions; using Content.Shared.Buckle; using Content.Shared.Damage; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Polymorph; using Content.Shared.Popups; using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Polymorph.Systems { public sealed partial class PolymorphSystem : EntitySystem { [Dependency] private readonly IComponentFactory _compFact = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _proto = 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!; private ISawmill _sawmill = default!; private const string RevertPolymorphId = "ActionRevertPolymorph"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnPolymorphActionEvent); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnBeforeFullyEaten); SubscribeLocalEvent(OnBeforeFullySliced); SubscribeLocalEvent(OnRevertPolymorphActionEvent); InitializeCollide(); InitializeMap(); _sawmill = Logger.GetSawmill("polymorph"); } private void OnStartup(EntityUid uid, PolymorphableComponent component, ComponentStartup args) { if (component.InnatePolymorphs != null) { foreach (var morph in component.InnatePolymorphs) { CreatePolymorphAction(morph, uid); } } } private void OnPolymorphActionEvent(EntityUid uid, PolymorphableComponent component, PolymorphActionEvent args) { PolymorphEntity(uid, args.Prototype); } private void OnRevertPolymorphActionEvent(EntityUid uid, PolymorphedEntityComponent component, RevertPolymorphActionEvent args) { Revert(uid, component); } public void OnStartup(EntityUid uid, PolymorphedEntityComponent component, ComponentStartup args) { if (!_proto.TryIndex(component.Prototype, out PolymorphPrototype? proto)) { // warning instead of error because of the all-comps one entity test. _sawmill.Warning($"{nameof(PolymorphSystem)} encountered an improperly set up polymorph component while initializing. Entity {ToPrettyString(uid)}. Prototype: {component.Prototype}"); RemCompDeferred(uid, component); return; } if (proto.Forced) return; var actionId = Spawn(RevertPolymorphId); if (_actions.TryGetActionData(actionId, out var action)) { action.EntityIcon = component.Parent; action.UseDelay = TimeSpan.FromSeconds(proto.Delay); } _actions.AddAction(uid, actionId, null, null, action); } private void OnBeforeFullyEaten(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullyEatenEvent args) { if (!_proto.TryIndex(comp.Prototype, out var proto)) { _sawmill.Error($"Invalid polymorph prototype {comp.Prototype}"); return; } if (proto.RevertOnEat) { args.Cancel(); Revert(uid, comp); } } private void OnBeforeFullySliced(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullySlicedEvent args) { if (!_proto.TryIndex(comp.Prototype, out var proto)) { _sawmill.Error("Invalid polymorph prototype {comp.Prototype}"); return; } if (proto.RevertOnEat) { args.Cancel(); Revert(uid, comp); } } /// /// 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 target, string id) { if (!_proto.TryIndex(id, out var proto)) { _sawmill.Error("Invalid polymorph prototype {id}"); return null; } return PolymorphEntity(target, proto); } /// /// Polymorphs the target entity into the specific polymorph prototype /// /// The entity that will be transformed /// The polymorph prototype public EntityUid? PolymorphEntity(EntityUid uid, PolymorphPrototype proto) { // if it's already morphed, don't allow it again with this condition active. if (!proto.AllowRepeatedMorphs && HasComp(uid)) return null; // mostly just for vehicles _buckle.TryUnbuckle(uid, uid, true); var targetTransformComp = Transform(uid); var child = Spawn(proto.Entity, targetTransformComp.Coordinates); MakeSentientCommand.MakeSentient(child, EntityManager); var comp = _compFact.GetComponent(); comp.Owner = child; comp.Parent = uid; comp.Prototype = proto.ID; EntityManager.AddComponent(child, comp); var childXform = Transform(child); childXform.LocalRotation = targetTransformComp.LocalRotation; if (_container.TryGetContainingContainer(uid, out var cont)) cont.Insert(child); //Transfers all damage from the original to the new one if (proto.TransferDamage && TryComp(child, out var damageParent) && _mobThreshold.GetScaledDamage(uid, child, out var damage) && damage != null) { _damageable.SetDamage(child, damageParent, damage); } if (proto.Inventory == PolymorphInventoryChange.Transfer) { _inventory.TransferEntityInventories(uid, child); foreach (var hand in _hands.EnumerateHeld(uid)) { _hands.TryDrop(uid, hand, checkActionBlocker: false); _hands.TryPickupAnyHand(child, hand); } } else if (proto.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 (proto.TransferName && TryComp(uid, out var targetMeta)) _metaData.SetEntityName(child, targetMeta.EntityName); if (proto.TransferHumanoidAppearance) { _humanoid.CloneAppearance(uid, child); } if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, child, mind: mind); //Ensures a map to banish the entity to EnsurePausesdMap(); if (PausedMap != null) _transform.SetParent(uid, targetTransformComp, PausedMap.Value); return child; } /// /// Reverts a polymorphed entity back into its original form /// /// The entityuid of the entity being reverted /// public EntityUid? Revert(EntityUid uid, PolymorphedEntityComponent? component = null) { if (Deleted(uid)) return null; if (!Resolve(uid, ref component)) return null; var parent = component.Parent; if (Deleted(parent)) return null; if (!_proto.TryIndex(component.Prototype, out PolymorphPrototype? proto)) { _sawmill.Error($"{nameof(PolymorphSystem)} encountered an improperly initialized polymorph component while reverting. Entity {ToPrettyString(uid)}. Prototype: {component.Prototype}"); return null; } var uidXform = Transform(uid); var parentXform = Transform(parent); _transform.SetParent(parent, parentXform, uidXform.ParentUid); parentXform.Coordinates = uidXform.Coordinates; parentXform.LocalRotation = uidXform.LocalRotation; if (proto.TransferDamage && TryComp(parent, out var damageParent) && _mobThreshold.GetScaledDamage(uid, parent, out var damage) && damage != null) { _damageable.SetDamage(parent, damageParent, damage); } if (proto.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 (proto.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); } } if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, parent, mind: mind); // if an item polymorph was picked up, put it back down after reverting Transform(parent).AttachToGridOrMap(); _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(string id, EntityUid target) { if (!_proto.TryIndex(id, out var polyproto)) { _sawmill.Error("Invalid polymorph prototype"); return; } if (!TryComp(target, out var polycomp)) return; var entproto = _proto.Index(polyproto.Entity); var actionId = Spawn(RevertPolymorphId); if (_actions.TryGetActionData(actionId, out var baseAction) && baseAction is InstantActionComponent action) { action.Event = new PolymorphActionEvent { Prototype = polyproto }; action.Icon = new SpriteSpecifier.EntityPrototype(polyproto.Entity); _metaData.SetEntityName(actionId, Loc.GetString("polymorph-self-action-name", ("target", entproto.Name))); _metaData.SetEntityDescription(actionId, Loc.GetString("polymorph-self-action-description", ("target", entproto.Name))); polycomp.PolymorphActions ??= new Dictionary(); polycomp.PolymorphActions.Add(id, actionId); _actions.AddAction(target, actionId, target); } } [PublicAPI] public void RemovePolymorphAction(string id, EntityUid target, PolymorphableComponent? component = null) { if (!Resolve(target, ref component, false)) return; if (component.PolymorphActions == null) return; if (component.PolymorphActions.TryGetValue(id, out var val)) _actions.RemoveAction(target, val); } 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 (!_proto.TryIndex(comp.Prototype, out PolymorphPrototype? proto)) { _sawmill.Error($"{nameof(PolymorphSystem)} encountered an improperly initialized polymorph component while updating. Entity {ToPrettyString(uid)}. Prototype: {comp.Prototype}"); RemCompDeferred(uid, comp); continue; } if (proto.Duration != null && comp.Time >= proto.Duration) { Revert(uid, comp); continue; } if (!TryComp(uid, out var mob)) continue; if (proto.RevertOnDeath && _mobState.IsDead(uid, mob) || proto.RevertOnCrit && _mobState.IsIncapacitated(uid, mob)) { Revert(uid, comp); } } UpdateCollide(); } } }