using Content.Shared.DoAfter; using Content.Shared.Eye; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.RatKing; using Content.Shared.Verbs; using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; namespace Content.Shared._DV.NodeCrawl; /// /// Manages entry & exit of node crawlers into node networks /// public abstract class SharedNodeCrawlSystem : EntitySystem { [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly NodeCrawlerMovementSystem _nodeCrawler = default!; [Dependency] private readonly EntityLookupSystem _entityLookup = default!; private const string MoverContainer = "mover-container"; private static readonly EntProtoId MoverProto = "DVNodeCrawlMover"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent>(OnGetVerbs); SubscribeLocalEvent(OnNodeCrawlEntryDoAfter); SubscribeLocalEvent(OnArrivedAtNode); SubscribeLocalEvent(OnGetVisMask); SubscribeLocalEvent(OnCrawlableShutdown); SubscribeLocalEvent(OnMovementShutdown); SubscribeLocalEvent(OnCrawlerShutdown); SubscribeLocalEvent(OnCrawlableAnchorChanged); SubscribeLocalEvent(OnRatKingStartedCrawling); } private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) { var user = args.User; var target = args.Target; if (!TryComp(user, out var nodeCrawler)) return; if (!_entityWhitelist.IsWhitelistPass(nodeCrawler.ExitNodes, target)) return; if (!args.CanAccess) return; args.Verbs.Add(new AlternativeVerb { Act = () => StartEntryDoAfter((user, nodeCrawler), target), Text = Loc.GetString("node-crawl-enter", ("target", target)), }); } private void StartEntryDoAfter(Entity ent, EntityUid target) { var doAfterArgs = new DoAfterArgs(EntityManager, ent.Owner, ent.Comp.EnterDelay, new NodeCrawlEnterDoAfterEvent(), ent.Owner, target); _doAfter.TryStartDoAfter(doAfterArgs); } private void OnNodeCrawlEntryDoAfter(Entity ent, ref NodeCrawlEnterDoAfterEvent args) { if (args.Cancelled || args.Target is not { } target) return; NodeCrawl(ent, target); } protected virtual void SetupAir(Entity movement) { } protected virtual void EjectAir(Entity movement) { } private void NodeCrawl(Entity ent, EntityUid target) { if (!_net.IsServer) return; var mover = Spawn(MoverProto, Transform(target).Coordinates); var crawler = Comp(mover); var container = _container.GetContainer(mover, MoverContainer); _container.Insert(ent.Owner, container); ent.Comp.Mover = mover; Dirty(ent); var evt = new NodeCrawlerStartedCrawlingEvent((mover, crawler)); RaiseLocalEvent(ent, ref evt); _nodeCrawler.SetNode((mover, crawler), target); _nodeCrawler.SetHeldCrawler((mover, crawler), ent); SetupAir((mover, crawler)); _mover.SetRelay(ent, mover); _physics.SetCanCollide(ent.Owner, false); _physics.SetCanCollide(mover, false); _eye.RefreshVisibilityMask(ent.Owner); } /// /// Causes this node crawler to exit its node crawl. /// /// The crawler to exit node-crawl from. public void ExitNodeCrawl(Entity ent) { if (ent.Comp.Mover is not { } mover) return; ent.Comp.Mover = null; Dirty(ent); var container = _container.GetContainer(mover, MoverContainer); _container.Remove(ent.Owner, container); foreach (var other in _container.EmptyContainer(container)) { if (!TryComp(other, out var otherCrawler)) continue; otherCrawler.Mover = null; Dirty(other, otherCrawler); } RemComp(ent); if (_net.IsServer && !TerminatingOrDeleted(mover)) { if (TryComp(mover, out var movement)) EjectAir((mover, movement)); QueueDel(mover); // deletion isn't predicted because client queued deletion doesn't interact well with container stuff } _physics.SetCanCollide(ent.Owner, true); _eye.RefreshVisibilityMask(ent.Owner); } private void OnArrivedAtNode(Entity ent, ref NodeCrawlerArrivedAtNodeEvent args) { if (!_entityWhitelist.IsWhitelistPass(ent.Comp.ExitNodes, args.Node)) return; ExitNodeCrawl(ent); } private void OnGetVisMask(Entity ent, ref GetVisMaskEvent args) { if (ent.Comp.Mover is null) return; args.VisibilityMask |= (int)VisibilityFlags.Subfloor; } private void OnCrawlableShutdown(Entity ent, ref ComponentShutdown args) { foreach (var crawler in ent.Comp.Crawlers) { var movement = Comp(crawler); if (movement.HeldCrawler is not { } held) continue; _nodeCrawler.SetNode((crawler, movement), null); ExitNodeCrawl((held, Comp(held))); } } private void OnMovementShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.Node is { } node && node.IsValid()) { var nodeComp = Comp(node); nodeComp.Crawlers.Remove(ent); Dirty(node, nodeComp); } if (ent.Comp.HeldCrawler is { } crawler && crawler.IsValid() && !TerminatingOrDeleted(crawler) && TryComp(crawler, out var nodeCrawler)) { ExitNodeCrawl((crawler, nodeCrawler)); } } private void OnCrawlerShutdown(Entity ent, ref ComponentShutdown args) { ExitNodeCrawl(ent); } private void OnCrawlableAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) { if (args.Anchored) return; foreach (var crawler in ent.Comp.Crawlers) { var movement = Comp(crawler); if (movement.HeldCrawler is not { } held) continue; ExitNodeCrawl((held, Comp(held))); } } private void OnRatKingStartedCrawling(Entity ent, ref NodeCrawlerStartedCrawlingEvent args) { var entities = new HashSet>(); _entityLookup.GetEntitiesInRange(Transform(ent).Coordinates, ent.Comp.VentCrawlRecruitRadius, entities); var container = _container.GetContainer(args.Mover, MoverContainer); foreach (var servant in entities) { if (servant.Comp.King != ent) continue; _container.Insert(servant.Owner, container); } } }