SharedGunSystem.ChamberMagazine.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.Containers.ItemSlots;
  3. using Content.Shared.Examine;
  4. using Content.Shared.Interaction;
  5. using Content.Shared.Interaction.Events;
  6. using Content.Shared.Verbs;
  7. using Content.Shared.Weapons.Ranged.Components;
  8. using Content.Shared.Weapons.Ranged.Events;
  9. using Robust.Shared.Containers;
  10. namespace Content.Shared.Weapons.Ranged.Systems;
  11. public abstract partial class SharedGunSystem
  12. {
  13. protected const string ChamberSlot = "gun_chamber";
  14. protected virtual void InitializeChamberMagazine()
  15. {
  16. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ComponentStartup>(OnChamberStartup);
  17. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, TakeAmmoEvent>(OnChamberMagazineTakeAmmo);
  18. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetAmmoCountEvent>(OnChamberAmmoCount);
  19. /*
  20. * Open and close bolts are separate verbs.
  21. * Racking does both in one hit and has a different sound (to avoid RSI + sounds cooler).
  22. */
  23. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetVerbsEvent<ActivationVerb>>(OnChamberActivationVerb);
  24. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetVerbsEvent<InteractionVerb>>(OnChamberInteractionVerb);
  25. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetVerbsEvent<AlternativeVerb>>(OnMagazineVerb);
  26. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ActivateInWorldEvent>(OnChamberActivate);
  27. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, UseInHandEvent>(OnChamberUse);
  28. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, EntInsertedIntoContainerMessage>(OnMagazineSlotChange);
  29. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, EntRemovedFromContainerMessage>(OnMagazineSlotChange);
  30. SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ExaminedEvent>(OnChamberMagazineExamine);
  31. }
  32. private void OnChamberStartup(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ComponentStartup args)
  33. {
  34. // Appearance data doesn't get serialized and want to make sure this is correct on spawn (regardless of MapInit) so.
  35. if (component.BoltClosed != null)
  36. {
  37. Appearance.SetData(uid, AmmoVisuals.BoltClosed, component.BoltClosed.Value);
  38. }
  39. }
  40. /// <summary>
  41. /// Called when user "Activated In World" (E) with the gun as the target
  42. /// </summary>
  43. private void OnChamberActivate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ActivateInWorldEvent args)
  44. {
  45. if (args.Handled || !args.Complex)
  46. return;
  47. args.Handled = true;
  48. ToggleBolt(uid, component, args.User);
  49. }
  50. /// <summary>
  51. /// Called when gun was "Activated In Hand" (Z)
  52. /// </summary>
  53. private void OnChamberUse(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UseInHandEvent args)
  54. {
  55. if (args.Handled)
  56. return;
  57. args.Handled = true;
  58. if (component.CanRack)
  59. UseChambered(uid, component, args.User);
  60. else
  61. ToggleBolt(uid, component, args.User);
  62. }
  63. /// <summary>
  64. /// Creates "Rack" verb on the gun
  65. /// </summary>
  66. private void OnChamberActivationVerb(EntityUid uid, ChamberMagazineAmmoProviderComponent component, GetVerbsEvent<ActivationVerb> args)
  67. {
  68. if (!args.CanAccess || !args.CanInteract || component.BoltClosed == null || !component.CanRack)
  69. return;
  70. args.Verbs.Add(new ActivationVerb()
  71. {
  72. Text = Loc.GetString("gun-chamber-rack"),
  73. Act = () =>
  74. {
  75. UseChambered(uid, component, args.User);
  76. }
  77. });
  78. }
  79. /// <summary>
  80. /// Opens then closes the bolt, or just closes it if currently open.
  81. /// </summary>
  82. private void UseChambered(EntityUid uid, ChamberMagazineAmmoProviderComponent component, EntityUid? user = null)
  83. {
  84. if (component.BoltClosed == false)
  85. {
  86. ToggleBolt(uid, component, user);
  87. return;
  88. }
  89. if (TryTakeChamberEntity(uid, out var chamberEnt))
  90. {
  91. if (_netManager.IsServer)
  92. {
  93. EjectCartridge(chamberEnt.Value);
  94. }
  95. else
  96. {
  97. // Similar to below just due to prediction.
  98. TransformSystem.DetachEntity(chamberEnt.Value, Transform(chamberEnt.Value));
  99. }
  100. }
  101. if (!CycleCartridge(uid, component, user))
  102. {
  103. UpdateAmmoCount(uid);
  104. }
  105. if (component.BoltClosed != false)
  106. {
  107. Audio.PlayPredicted(component.RackSound, uid, user);
  108. }
  109. }
  110. /// <summary>
  111. /// Creates "Open/Close bolt" verb on the gun
  112. /// </summary>
  113. private void OnChamberInteractionVerb(EntityUid uid, ChamberMagazineAmmoProviderComponent component, GetVerbsEvent<InteractionVerb> args)
  114. {
  115. if (!args.CanAccess || !args.CanInteract || component.BoltClosed == null)
  116. return;
  117. args.Verbs.Add(new InteractionVerb()
  118. {
  119. Text = component.BoltClosed.Value ? Loc.GetString("gun-chamber-bolt-open") : Loc.GetString("gun-chamber-bolt-close"),
  120. Act = () =>
  121. {
  122. // Just toggling might be more user friendly instead of trying to set to whatever they think?
  123. ToggleBolt(uid, component, args.User);
  124. }
  125. });
  126. }
  127. /// <summary>
  128. /// Updates the bolt to its new state
  129. /// </summary>
  130. public void SetBoltClosed(EntityUid uid, ChamberMagazineAmmoProviderComponent component, bool value, EntityUid? user = null, AppearanceComponent? appearance = null, ItemSlotsComponent? slots = null)
  131. {
  132. if (component.BoltClosed == null || value == component.BoltClosed)
  133. return;
  134. Resolve(uid, ref appearance, ref slots, false);
  135. Appearance.SetData(uid, AmmoVisuals.BoltClosed, value, appearance);
  136. if (value)
  137. {
  138. CycleCartridge(uid, component, user, appearance);
  139. if (user != null)
  140. PopupSystem.PopupClient(Loc.GetString("gun-chamber-bolt-closed"), uid, user.Value);
  141. if (slots != null)
  142. {
  143. _slots.SetLock(uid, ChamberSlot, true, slots);
  144. }
  145. Audio.PlayPredicted(component.BoltClosedSound, uid, user);
  146. }
  147. else
  148. {
  149. if (TryTakeChamberEntity(uid, out var chambered))
  150. {
  151. if (_netManager.IsServer)
  152. {
  153. EjectCartridge(chambered.Value);
  154. }
  155. else
  156. {
  157. // Prediction moment
  158. // The problem is client will dump the cartridge on the ground and the new server state
  159. // won't correspond due to randomness so looks weird
  160. // but we also need to always take it from the chamber or else ammocount won't be correct.
  161. TransformSystem.DetachParentToNull(chambered.Value, Transform(chambered.Value));
  162. }
  163. UpdateAmmoCount(uid);
  164. }
  165. if (user != null)
  166. PopupSystem.PopupClient(Loc.GetString("gun-chamber-bolt-opened"), uid, user.Value);
  167. if (slots != null)
  168. {
  169. _slots.SetLock(uid, ChamberSlot, false, slots);
  170. }
  171. Audio.PlayPredicted(component.BoltOpenedSound, uid, user);
  172. }
  173. component.BoltClosed = value;
  174. Dirty(uid, component);
  175. }
  176. /// <summary>
  177. /// Tries to take ammo from the magazine and insert into the chamber.
  178. /// </summary>
  179. private bool CycleCartridge(EntityUid uid, ChamberMagazineAmmoProviderComponent component, EntityUid? user = null, AppearanceComponent? appearance = null)
  180. {
  181. // Try to put a new round in if possible.
  182. var magEnt = GetMagazineEntity(uid);
  183. var chambered = GetChamberEntity(uid);
  184. var result = false;
  185. // Similar to what takeammo does though that uses an optimised version where
  186. // multiple bullets may be fired in a single tick.
  187. if (magEnt != null && chambered == null)
  188. {
  189. var relayedArgs = new TakeAmmoEvent(1,
  190. new List<(EntityUid? Entity, IShootable Shootable)>(),
  191. Transform(uid).Coordinates,
  192. user);
  193. RaiseLocalEvent(magEnt.Value, relayedArgs);
  194. if (relayedArgs.Ammo.Count > 0)
  195. {
  196. var newChamberEnt = relayedArgs.Ammo[0].Entity;
  197. TryInsertChamber(uid, newChamberEnt!.Value);
  198. var ammoEv = new GetAmmoCountEvent();
  199. RaiseLocalEvent(magEnt.Value, ref ammoEv);
  200. FinaliseMagazineTakeAmmo(uid, component, ammoEv.Count, ammoEv.Capacity, user, appearance);
  201. UpdateAmmoCount(uid);
  202. // Clientside reconciliation things
  203. if (_netManager.IsClient)
  204. {
  205. foreach (var (ent, _) in relayedArgs.Ammo)
  206. {
  207. if (!IsClientSide(ent!.Value))
  208. continue;
  209. Del(ent.Value);
  210. }
  211. }
  212. }
  213. else
  214. {
  215. UpdateAmmoCount(uid);
  216. }
  217. result = true;
  218. }
  219. return result;
  220. }
  221. /// <summary>
  222. /// Sets the bolt's positional value to the other state
  223. /// </summary>
  224. public void ToggleBolt(EntityUid uid, ChamberMagazineAmmoProviderComponent component, EntityUid? user = null)
  225. {
  226. if (component.BoltClosed == null)
  227. return;
  228. SetBoltClosed(uid, component, !component.BoltClosed.Value, user);
  229. }
  230. /// <summary>
  231. /// Called when the gun was Examined
  232. /// </summary>
  233. private void OnChamberMagazineExamine(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ExaminedEvent args)
  234. {
  235. if (!args.IsInDetailsRange)
  236. return;
  237. var (count, _) = GetChamberMagazineCountCapacity(uid, component);
  238. string boltState;
  239. using (args.PushGroup(nameof(ChamberMagazineAmmoProviderComponent)))
  240. {
  241. if (component.BoltClosed != null)
  242. {
  243. if (component.BoltClosed == true)
  244. boltState = Loc.GetString("gun-chamber-bolt-open-state");
  245. else
  246. boltState = Loc.GetString("gun-chamber-bolt-closed-state");
  247. args.PushMarkup(Loc.GetString("gun-chamber-bolt", ("bolt", boltState),
  248. ("color", component.BoltClosed.Value ? Color.FromHex("#94e1f2") : Color.FromHex("#f29d94"))));
  249. }
  250. args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count)));
  251. }
  252. }
  253. private bool TryTakeChamberEntity(EntityUid uid, [NotNullWhen(true)] out EntityUid? entity)
  254. {
  255. if (!Containers.TryGetContainer(uid, ChamberSlot, out var container) ||
  256. container is not ContainerSlot slot)
  257. {
  258. entity = null;
  259. return false;
  260. }
  261. entity = slot.ContainedEntity;
  262. if (entity == null)
  263. return false;
  264. Containers.Remove(entity.Value, container);
  265. return true;
  266. }
  267. protected EntityUid? GetChamberEntity(EntityUid uid)
  268. {
  269. if (!Containers.TryGetContainer(uid, ChamberSlot, out var container) ||
  270. container is not ContainerSlot slot)
  271. {
  272. return null;
  273. }
  274. return slot.ContainedEntity;
  275. }
  276. protected (int, int) GetChamberMagazineCountCapacity(EntityUid uid, ChamberMagazineAmmoProviderComponent component)
  277. {
  278. var count = GetChamberEntity(uid) != null ? 1 : 0;
  279. var (magCount, magCapacity) = GetMagazineCountCapacity(uid, component);
  280. return (count + magCount, magCapacity);
  281. }
  282. private bool TryInsertChamber(EntityUid uid, EntityUid ammo)
  283. {
  284. return Containers.TryGetContainer(uid, ChamberSlot, out var container) &&
  285. container is ContainerSlot slot &&
  286. Containers.Insert(ammo, slot);
  287. }
  288. private void OnChamberAmmoCount(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ref GetAmmoCountEvent args)
  289. {
  290. OnMagazineAmmoCount(uid, component, ref args);
  291. args.Capacity += 1;
  292. var chambered = GetChamberEntity(uid);
  293. if (chambered != null)
  294. {
  295. args.Count += 1;
  296. }
  297. }
  298. private void OnChamberMagazineTakeAmmo(EntityUid uid, ChamberMagazineAmmoProviderComponent component, TakeAmmoEvent args)
  299. {
  300. if (component.BoltClosed == false)
  301. {
  302. args.Reason = Loc.GetString("gun-chamber-bolt-ammo");
  303. return;
  304. }
  305. // So chamber logic is kinda sussier than the others
  306. // Essentially we want to treat the chamber as a potentially free slot and then the mag as the remaining slots
  307. // i.e. if we shoot 3 times, then we use the chamber once (regardless if it's empty or not) and 2 from the mag
  308. // We move the n + 1 shot into the chamber as we essentially treat it like a stack.
  309. TryComp<AppearanceComponent>(uid, out var appearance);
  310. EntityUid? chamberEnt;
  311. // Normal behaviour for guns.
  312. if (component.AutoCycle)
  313. {
  314. if (TryTakeChamberEntity(uid, out chamberEnt))
  315. {
  316. args.Ammo.Add((chamberEnt.Value, EnsureShootable(chamberEnt.Value)));
  317. }
  318. // No ammo returned.
  319. else
  320. {
  321. return;
  322. }
  323. var magEnt = GetMagazineEntity(uid);
  324. // Pass an event to the magazine to get more (to refill chamber or for shooting).
  325. if (magEnt != null)
  326. {
  327. // We pass in Shots not Shots - 1 as we'll take the last entity and move it into the chamber.
  328. var relayedArgs = new TakeAmmoEvent(args.Shots, new List<(EntityUid? Entity, IShootable Shootable)>(), args.Coordinates, args.User);
  329. RaiseLocalEvent(magEnt.Value, relayedArgs);
  330. // Put in the nth slot back into the chamber
  331. // Rest of the ammo gets shot
  332. if (relayedArgs.Ammo.Count > 0)
  333. {
  334. var newChamberEnt = relayedArgs.Ammo[^1].Entity;
  335. TryInsertChamber(uid, newChamberEnt!.Value);
  336. }
  337. // Anything above the chamber-refill amount gets fired.
  338. for (var i = 0; i < relayedArgs.Ammo.Count - 1; i++)
  339. {
  340. args.Ammo.Add(relayedArgs.Ammo[i]);
  341. }
  342. // If no more ammo then open bolt.
  343. if (relayedArgs.Ammo.Count == 0)
  344. {
  345. SetBoltClosed(uid, component, false, user: args.User, appearance: appearance);
  346. }
  347. }
  348. else
  349. {
  350. Appearance.SetData(uid, AmmoVisuals.MagLoaded, false, appearance);
  351. return;
  352. }
  353. var ammoEv = new GetAmmoCountEvent();
  354. RaiseLocalEvent(magEnt.Value, ref ammoEv);
  355. FinaliseMagazineTakeAmmo(uid, component, ammoEv.Count, ammoEv.Capacity, args.User, appearance);
  356. }
  357. // If gun doesn't autocycle (e.g. bolt-action weapons) then we leave the chambered entity in there but still return it.
  358. else if (Containers.TryGetContainer(uid, ChamberSlot, out var container) &&
  359. container is ContainerSlot { ContainedEntity: not null } slot)
  360. {
  361. // Shooting code won't eject it if it's still contained.
  362. chamberEnt = slot.ContainedEntity;
  363. args.Ammo.Add((chamberEnt.Value, EnsureShootable(chamberEnt.Value)));
  364. }
  365. }
  366. }