SharedEntityStorageSystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Shared.Body.Components;
  5. using Content.Shared.Destructible;
  6. using Content.Shared.Foldable;
  7. using Content.Shared.Hands.Components;
  8. using Content.Shared.Interaction;
  9. using Content.Shared.Item;
  10. using Content.Shared.Lock;
  11. using Content.Shared.Movement.Events;
  12. using Content.Shared.Popups;
  13. using Content.Shared.Storage.Components;
  14. using Content.Shared.Tools.Systems;
  15. using Content.Shared.Verbs;
  16. using Content.Shared.Wall;
  17. using Content.Shared.Whitelist;
  18. using Content.Shared.ActionBlocker;
  19. using Robust.Shared.Audio.Systems;
  20. using Robust.Shared.Containers;
  21. using Robust.Shared.GameStates;
  22. using Robust.Shared.Map;
  23. using Robust.Shared.Network;
  24. using Robust.Shared.Physics;
  25. using Robust.Shared.Physics.Systems;
  26. using Robust.Shared.Timing;
  27. using Robust.Shared.Utility;
  28. namespace Content.Shared.Storage.EntitySystems;
  29. public abstract class SharedEntityStorageSystem : EntitySystem
  30. {
  31. [Dependency] private readonly IGameTiming _timing = default!;
  32. [Dependency] private readonly INetManager _net = default!;
  33. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  34. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  35. [Dependency] private readonly SharedAudioSystem _audio = default!;
  36. [Dependency] private readonly SharedContainerSystem _container = default!;
  37. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  38. [Dependency] private readonly SharedJointSystem _joints = default!;
  39. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  40. [Dependency] protected readonly SharedPopupSystem Popup = default!;
  41. [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
  42. [Dependency] private readonly WeldableSystem _weldable = default!;
  43. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  44. [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
  45. public const string ContainerName = "entity_storage";
  46. protected void OnEntityUnpausedEvent(EntityUid uid, SharedEntityStorageComponent component, EntityUnpausedEvent args)
  47. {
  48. component.NextInternalOpenAttempt += args.PausedTime;
  49. }
  50. protected void OnGetState(EntityUid uid, SharedEntityStorageComponent component, ref ComponentGetState args)
  51. {
  52. args.State = new EntityStorageComponentState(component.Open,
  53. component.Capacity,
  54. component.IsCollidableWhenOpen,
  55. component.OpenOnMove,
  56. component.EnteringRange,
  57. component.NextInternalOpenAttempt);
  58. }
  59. protected void OnHandleState(EntityUid uid, SharedEntityStorageComponent component, ref ComponentHandleState args)
  60. {
  61. if (args.Current is not EntityStorageComponentState state)
  62. return;
  63. component.Open = state.Open;
  64. component.Capacity = state.Capacity;
  65. component.IsCollidableWhenOpen = state.IsCollidableWhenOpen;
  66. component.OpenOnMove = state.OpenOnMove;
  67. component.EnteringRange = state.EnteringRange;
  68. component.NextInternalOpenAttempt = state.NextInternalOpenAttempt;
  69. }
  70. protected virtual void OnComponentInit(EntityUid uid, SharedEntityStorageComponent component, ComponentInit args)
  71. {
  72. component.Contents = _container.EnsureContainer<Container>(uid, ContainerName);
  73. component.Contents.ShowContents = component.ShowContents;
  74. component.Contents.OccludesLight = component.OccludesLight;
  75. }
  76. protected virtual void OnComponentStartup(EntityUid uid, SharedEntityStorageComponent component, ComponentStartup args)
  77. {
  78. _appearance.SetData(uid, StorageVisuals.Open, component.Open);
  79. }
  80. protected void OnInteract(EntityUid uid, SharedEntityStorageComponent component, ActivateInWorldEvent args)
  81. {
  82. if (args.Handled || !args.Complex)
  83. return;
  84. args.Handled = true;
  85. ToggleOpen(args.User, uid, component);
  86. }
  87. public abstract bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref SharedEntityStorageComponent? component);
  88. protected void OnLockToggleAttempt(EntityUid uid, SharedEntityStorageComponent target, ref LockToggleAttemptEvent args)
  89. {
  90. // Cannot (un)lock open lockers.
  91. if (target.Open)
  92. args.Cancelled = true;
  93. // Cannot (un)lock from the inside. Maybe a bad idea? Security jocks could trap nerds in lockers?
  94. if (target.Contents.Contains(args.User))
  95. args.Cancelled = true;
  96. }
  97. protected void OnDestruction(EntityUid uid, SharedEntityStorageComponent component, DestructionEventArgs args)
  98. {
  99. component.Open = true;
  100. Dirty(uid, component);
  101. if (!component.DeleteContentsOnDestruction)
  102. {
  103. EmptyContents(uid, component);
  104. return;
  105. }
  106. foreach (var ent in new List<EntityUid>(component.Contents.ContainedEntities))
  107. {
  108. Del(ent);
  109. }
  110. }
  111. protected void OnRelayMovement(EntityUid uid, SharedEntityStorageComponent component, ref ContainerRelayMovementEntityEvent args)
  112. {
  113. if (!HasComp<HandsComponent>(args.Entity))
  114. return;
  115. if (!_actionBlocker.CanMove(args.Entity))
  116. return;
  117. if (_timing.CurTime < component.NextInternalOpenAttempt)
  118. return;
  119. component.NextInternalOpenAttempt = _timing.CurTime + SharedEntityStorageComponent.InternalOpenAttemptDelay;
  120. Dirty(uid, component);
  121. if (component.OpenOnMove)
  122. TryOpenStorage(args.Entity, uid);
  123. }
  124. protected void OnFoldAttempt(EntityUid uid, SharedEntityStorageComponent component, ref FoldAttemptEvent args)
  125. {
  126. if (args.Cancelled)
  127. return;
  128. args.Cancelled = component.Open || component.Contents.ContainedEntities.Count != 0;
  129. }
  130. protected void AddToggleOpenVerb(EntityUid uid, SharedEntityStorageComponent component, GetVerbsEvent<InteractionVerb> args)
  131. {
  132. if (!args.CanAccess || !args.CanInteract)
  133. return;
  134. if (!CanOpen(args.User, args.Target, silent: true, component))
  135. return;
  136. InteractionVerb verb = new();
  137. if (component.Open)
  138. {
  139. verb.Text = Loc.GetString("verb-common-close");
  140. verb.Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/close.svg.192dpi.png"));
  141. }
  142. else
  143. {
  144. verb.Text = Loc.GetString("verb-common-open");
  145. verb.Icon = new SpriteSpecifier.Texture(
  146. new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"));
  147. }
  148. verb.Act = () => ToggleOpen(args.User, args.Target, component);
  149. args.Verbs.Add(verb);
  150. }
  151. public void ToggleOpen(EntityUid user, EntityUid target, SharedEntityStorageComponent? component = null)
  152. {
  153. if (!ResolveStorage(target, ref component))
  154. return;
  155. if (component.Open)
  156. {
  157. TryCloseStorage(target);
  158. }
  159. else
  160. {
  161. TryOpenStorage(user, target);
  162. }
  163. }
  164. public void EmptyContents(EntityUid uid, SharedEntityStorageComponent? component = null)
  165. {
  166. if (!ResolveStorage(uid, ref component))
  167. return;
  168. var uidXform = Transform(uid);
  169. var containedArr = component.Contents.ContainedEntities.ToArray();
  170. foreach (var contained in containedArr)
  171. {
  172. Remove(contained, uid, component, uidXform);
  173. }
  174. }
  175. public void OpenStorage(EntityUid uid, SharedEntityStorageComponent? component = null)
  176. {
  177. if (!ResolveStorage(uid, ref component))
  178. return;
  179. if (component.Open)
  180. return;
  181. var beforeev = new StorageBeforeOpenEvent();
  182. RaiseLocalEvent(uid, ref beforeev);
  183. component.Open = true;
  184. Dirty(uid, component);
  185. EmptyContents(uid, component);
  186. ModifyComponents(uid, component);
  187. if (_net.IsClient && _timing.IsFirstTimePredicted)
  188. _audio.PlayPvs(component.OpenSound, uid);
  189. ReleaseGas(uid, component);
  190. var afterev = new StorageAfterOpenEvent();
  191. RaiseLocalEvent(uid, ref afterev);
  192. }
  193. public void CloseStorage(EntityUid uid, SharedEntityStorageComponent? component = null)
  194. {
  195. if (!ResolveStorage(uid, ref component))
  196. return;
  197. if (!component.Open)
  198. return;
  199. // Prevent the container from closing if it is queued for deletion. This is so that the container-emptying
  200. // behaviour of DestructionEventArgs is respected. This exists because malicious players were using
  201. // destructible boxes to delete entities by having two players simultaneously destroy and close the box in
  202. // the same tick.
  203. if (EntityManager.IsQueuedForDeletion(uid))
  204. return;
  205. component.Open = false;
  206. Dirty(uid, component);
  207. var entities = _lookup.GetEntitiesInRange(
  208. new EntityCoordinates(uid, component.EnteringOffset),
  209. component.EnteringRange,
  210. LookupFlags.Approximate | LookupFlags.Dynamic | LookupFlags.Sundries
  211. );
  212. // Don't insert the container into itself.
  213. entities.Remove(uid);
  214. var ev = new StorageBeforeCloseEvent(entities, []);
  215. RaiseLocalEvent(uid, ref ev);
  216. foreach (var entity in ev.Contents)
  217. {
  218. if (!ev.BypassChecks.Contains(entity) && !CanInsert(entity, uid, component))
  219. continue;
  220. if (!AddToContents(entity, uid, component))
  221. continue;
  222. if (component.Contents.ContainedEntities.Count >= component.Capacity)
  223. break;
  224. }
  225. TakeGas(uid, component);
  226. ModifyComponents(uid, component);
  227. if (_net.IsClient && _timing.IsFirstTimePredicted)
  228. _audio.PlayPvs(component.CloseSound, uid);
  229. var afterev = new StorageAfterCloseEvent();
  230. RaiseLocalEvent(uid, ref afterev);
  231. }
  232. public bool Insert(EntityUid toInsert, EntityUid container, SharedEntityStorageComponent? component = null)
  233. {
  234. if (!ResolveStorage(container, ref component))
  235. return false;
  236. if (component.Open)
  237. {
  238. TransformSystem.DropNextTo(toInsert, container);
  239. return true;
  240. }
  241. _joints.RecursiveClearJoints(toInsert);
  242. if (!_container.Insert(toInsert, component.Contents))
  243. return false;
  244. var inside = EnsureComp<InsideEntityStorageComponent>(toInsert);
  245. inside.Storage = container;
  246. return true;
  247. }
  248. public bool Remove(EntityUid toRemove, EntityUid container, SharedEntityStorageComponent? component = null, TransformComponent? xform = null)
  249. {
  250. if (!Resolve(container, ref xform, false))
  251. return false;
  252. if (!ResolveStorage(container, ref component))
  253. return false;
  254. _container.Remove(toRemove, component.Contents);
  255. if (_container.IsEntityInContainer(container))
  256. {
  257. if (_container.TryGetOuterContainer(container, Transform(container), out var outerContainer) &&
  258. !HasComp<HandsComponent>(outerContainer.Owner))
  259. {
  260. _container.Insert(toRemove, outerContainer);
  261. return true;
  262. }
  263. }
  264. RemComp<InsideEntityStorageComponent>(toRemove);
  265. var pos = TransformSystem.GetWorldPosition(xform) + TransformSystem.GetWorldRotation(xform).RotateVec(component.EnteringOffset);
  266. TransformSystem.SetWorldPosition(toRemove, pos);
  267. return true;
  268. }
  269. public bool CanInsert(EntityUid toInsert, EntityUid container, SharedEntityStorageComponent? component = null)
  270. {
  271. if (!ResolveStorage(container, ref component))
  272. return false;
  273. if (component.Open)
  274. return true;
  275. if (component.Contents.ContainedEntities.Count >= component.Capacity)
  276. return false;
  277. var aabb = _lookup.GetAABBNoContainer(toInsert, Vector2.Zero, 0);
  278. if (component.MaxSize < aabb.Size.X || component.MaxSize < aabb.Size.Y)
  279. return false;
  280. // Allow other systems to prevent inserting the item: e.g. the item is actually a ghost.
  281. var attemptEvent = new InsertIntoEntityStorageAttemptEvent(toInsert);
  282. RaiseLocalEvent(toInsert, ref attemptEvent);
  283. if (attemptEvent.Cancelled)
  284. return false;
  285. // Consult the whitelist. The whitelist ignores the default assumption about how entity storage works.
  286. if (component.Whitelist != null)
  287. return _whitelistSystem.IsValid(component.Whitelist, toInsert);
  288. // The inserted entity must be a mob or an item.
  289. return HasComp<BodyComponent>(toInsert) || HasComp<ItemComponent>(toInsert);
  290. }
  291. public bool TryOpenStorage(EntityUid user, EntityUid target, bool silent = false)
  292. {
  293. if (!CanOpen(user, target, silent))
  294. return false;
  295. OpenStorage(target);
  296. return true;
  297. }
  298. public bool TryCloseStorage(EntityUid target)
  299. {
  300. if (!CanClose(target))
  301. {
  302. return false;
  303. }
  304. CloseStorage(target);
  305. return true;
  306. }
  307. public bool IsOpen(EntityUid target, SharedEntityStorageComponent? component = null)
  308. {
  309. if (!ResolveStorage(target, ref component))
  310. return false;
  311. return component.Open;
  312. }
  313. public bool CanOpen(EntityUid user, EntityUid target, bool silent = false, SharedEntityStorageComponent? component = null)
  314. {
  315. if (!ResolveStorage(target, ref component))
  316. return false;
  317. if (!HasComp<HandsComponent>(user))
  318. return false;
  319. if (_weldable.IsWelded(target))
  320. {
  321. if (!silent && !component.Contents.Contains(user))
  322. Popup.PopupClient(Loc.GetString("entity-storage-component-welded-shut-message"), target, user);
  323. return false;
  324. }
  325. //Checks to see if the opening position, if offset, is inside of a wall.
  326. if (component.EnteringOffset != new Vector2(0, 0) && !HasComp<WallMountComponent>(target)) //if the entering position is offset
  327. {
  328. var newCoords = new EntityCoordinates(target, component.EnteringOffset);
  329. if (!_interaction.InRangeUnobstructed(target, newCoords, 0, collisionMask: component.EnteringOffsetCollisionFlags))
  330. {
  331. if (!silent && _net.IsServer)
  332. Popup.PopupEntity(Loc.GetString("entity-storage-component-cannot-open-no-space"), target);
  333. return false;
  334. }
  335. }
  336. var ev = new StorageOpenAttemptEvent(user, silent);
  337. RaiseLocalEvent(target, ref ev, true);
  338. return !ev.Cancelled;
  339. }
  340. public bool CanClose(EntityUid target, bool silent = false)
  341. {
  342. var ev = new StorageCloseAttemptEvent();
  343. RaiseLocalEvent(target, ref ev, silent);
  344. return !ev.Cancelled;
  345. }
  346. public bool AddToContents(EntityUid toAdd, EntityUid container, SharedEntityStorageComponent? component = null)
  347. {
  348. if (!ResolveStorage(container, ref component))
  349. return false;
  350. if (toAdd == container)
  351. return false;
  352. return Insert(toAdd, container, component);
  353. }
  354. private void ModifyComponents(EntityUid uid, SharedEntityStorageComponent? component = null)
  355. {
  356. if (!ResolveStorage(uid, ref component))
  357. return;
  358. if (!component.IsCollidableWhenOpen && TryComp<FixturesComponent>(uid, out var fixtures) &&
  359. fixtures.Fixtures.Count > 0)
  360. {
  361. // currently only works for single-fixture entities. If they have more than one fixture, then
  362. // RemovedMasks needs to be tracked separately for each fixture, using a fixture Id Dictionary. Also the
  363. // fixture IDs probably cant be automatically generated without causing issues, unless there is some
  364. // guarantee that they will get deserialized with the same auto-generated ID when saving+loading the map.
  365. var fixture = fixtures.Fixtures.First();
  366. if (component.Open)
  367. {
  368. component.RemovedMasks = fixture.Value.CollisionLayer & component.MasksToRemove;
  369. _physics.SetCollisionLayer(uid, fixture.Key, fixture.Value, fixture.Value.CollisionLayer & ~component.MasksToRemove,
  370. manager: fixtures);
  371. }
  372. else
  373. {
  374. _physics.SetCollisionLayer(uid, fixture.Key, fixture.Value, fixture.Value.CollisionLayer | component.RemovedMasks,
  375. manager: fixtures);
  376. component.RemovedMasks = 0;
  377. }
  378. }
  379. _appearance.SetData(uid, StorageVisuals.Open, component.Open);
  380. _appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0);
  381. }
  382. protected virtual void TakeGas(EntityUid uid, SharedEntityStorageComponent component)
  383. {
  384. }
  385. public virtual void ReleaseGas(EntityUid uid, SharedEntityStorageComponent component)
  386. {
  387. }
  388. }