SharedGunSystem.Revolver.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. using Content.Shared.Interaction;
  2. using Content.Shared.Verbs;
  3. using Content.Shared.Weapons.Ranged.Components;
  4. using Content.Shared.Weapons.Ranged.Events;
  5. using Robust.Shared.Containers;
  6. using Robust.Shared.GameStates;
  7. using Robust.Shared.Serialization;
  8. using Robust.Shared.Utility;
  9. using System;
  10. using System.Linq;
  11. using Content.Shared.Interaction.Events;
  12. using Content.Shared.Wieldable;
  13. using Content.Shared.Wieldable.Components;
  14. using JetBrains.Annotations;
  15. namespace Content.Shared.Weapons.Ranged.Systems;
  16. public partial class SharedGunSystem
  17. {
  18. protected const string RevolverContainer = "revolver-ammo";
  19. protected virtual void InitializeRevolver()
  20. {
  21. SubscribeLocalEvent<RevolverAmmoProviderComponent, ComponentGetState>(OnRevolverGetState);
  22. SubscribeLocalEvent<RevolverAmmoProviderComponent, ComponentHandleState>(OnRevolverHandleState);
  23. SubscribeLocalEvent<RevolverAmmoProviderComponent, ComponentInit>(OnRevolverInit);
  24. SubscribeLocalEvent<RevolverAmmoProviderComponent, TakeAmmoEvent>(OnRevolverTakeAmmo);
  25. SubscribeLocalEvent<RevolverAmmoProviderComponent, GetVerbsEvent<AlternativeVerb>>(OnRevolverVerbs);
  26. SubscribeLocalEvent<RevolverAmmoProviderComponent, InteractUsingEvent>(OnRevolverInteractUsing);
  27. SubscribeLocalEvent<RevolverAmmoProviderComponent, GetAmmoCountEvent>(OnRevolverGetAmmoCount);
  28. SubscribeLocalEvent<RevolverAmmoProviderComponent, UseInHandEvent>(OnRevolverUse);
  29. }
  30. private void OnRevolverUse(EntityUid uid, RevolverAmmoProviderComponent component, UseInHandEvent args)
  31. {
  32. if (args.Handled)
  33. return;
  34. if (!_useDelay.TryResetDelay(uid))
  35. return;
  36. args.Handled = true;
  37. Cycle(component);
  38. UpdateAmmoCount(uid, prediction: false);
  39. Dirty(uid, component);
  40. }
  41. private void OnRevolverGetAmmoCount(EntityUid uid, RevolverAmmoProviderComponent component, ref GetAmmoCountEvent args)
  42. {
  43. args.Count += GetRevolverCount(component);
  44. args.Capacity += component.Capacity;
  45. }
  46. private void OnRevolverInteractUsing(EntityUid uid, RevolverAmmoProviderComponent component, InteractUsingEvent args)
  47. {
  48. if (args.Handled)
  49. return;
  50. if (TryRevolverInsert(uid, component, args.Used, args.User))
  51. args.Handled = true;
  52. }
  53. private void OnRevolverGetState(EntityUid uid, RevolverAmmoProviderComponent component, ref ComponentGetState args)
  54. {
  55. args.State = new RevolverAmmoProviderComponentState
  56. {
  57. CurrentIndex = component.CurrentIndex,
  58. AmmoSlots = GetNetEntityList(component.AmmoSlots),
  59. Chambers = component.Chambers,
  60. };
  61. }
  62. private void OnRevolverHandleState(EntityUid uid, RevolverAmmoProviderComponent component, ref ComponentHandleState args)
  63. {
  64. if (args.Current is not RevolverAmmoProviderComponentState state)
  65. return;
  66. var oldIndex = component.CurrentIndex;
  67. component.CurrentIndex = state.CurrentIndex;
  68. component.Chambers = new bool?[state.Chambers.Length];
  69. // Need to copy across the state rather than the ref.
  70. for (var i = 0; i < component.AmmoSlots.Count; i++)
  71. {
  72. component.AmmoSlots[i] = EnsureEntity<RevolverAmmoProviderComponent>(state.AmmoSlots[i], uid);
  73. component.Chambers[i] = state.Chambers[i];
  74. }
  75. // Handle spins
  76. if (oldIndex != state.CurrentIndex)
  77. {
  78. UpdateAmmoCount(uid, prediction: false);
  79. }
  80. }
  81. public bool TryRevolverInsert(EntityUid revolverUid, RevolverAmmoProviderComponent component, EntityUid uid, EntityUid? user)
  82. {
  83. if (_whitelistSystem.IsWhitelistFail(component.Whitelist, uid))
  84. return false;
  85. // If it's a speedloader try to get ammo from it.
  86. if (EntityManager.HasComponent<SpeedLoaderComponent>(uid))
  87. {
  88. var freeSlots = 0;
  89. for (var i = 0; i < component.Capacity; i++)
  90. {
  91. if (component.AmmoSlots[i] != null || component.Chambers[i] != null)
  92. continue;
  93. freeSlots++;
  94. }
  95. if (freeSlots == 0)
  96. {
  97. Popup(Loc.GetString("gun-revolver-full"), revolverUid, user);
  98. return false;
  99. }
  100. var xformQuery = GetEntityQuery<TransformComponent>();
  101. var xform = xformQuery.GetComponent(uid);
  102. var ammo = new List<(EntityUid? Entity, IShootable Shootable)>(freeSlots);
  103. var ev = new TakeAmmoEvent(freeSlots, ammo, xform.Coordinates, user);
  104. RaiseLocalEvent(uid, ev);
  105. if (ev.Ammo.Count == 0)
  106. {
  107. Popup(Loc.GetString("gun-speedloader-empty"), revolverUid, user);
  108. return false;
  109. }
  110. for (var i = Math.Min(ev.Ammo.Count - 1, component.Capacity - 1); i >= 0; i--)
  111. {
  112. var index = (component.CurrentIndex + i) % component.Capacity;
  113. if (component.AmmoSlots[index] != null ||
  114. component.Chambers[index] != null)
  115. {
  116. continue;
  117. }
  118. var ent = ev.Ammo.Last().Entity;
  119. ev.Ammo.RemoveAt(ev.Ammo.Count - 1);
  120. if (ent == null)
  121. {
  122. Log.Error($"Tried to load hitscan into a revolver which is unsupported");
  123. continue;
  124. }
  125. component.AmmoSlots[index] = ent.Value;
  126. Containers.Insert(ent.Value, component.AmmoContainer);
  127. SetChamber(index, component, uid);
  128. if (ev.Ammo.Count == 0)
  129. break;
  130. }
  131. DebugTools.Assert(ammo.Count == 0);
  132. UpdateRevolverAppearance(revolverUid, component);
  133. UpdateAmmoCount(revolverUid);
  134. Dirty(revolverUid, component);
  135. Audio.PlayPredicted(component.SoundInsert, revolverUid, user);
  136. Popup(Loc.GetString("gun-revolver-insert"), revolverUid, user);
  137. return true;
  138. }
  139. // Try to insert the entity directly.
  140. for (var i = 0; i < component.Capacity; i++)
  141. {
  142. var index = (component.CurrentIndex + i) % component.Capacity;
  143. if (component.AmmoSlots[index] != null ||
  144. component.Chambers[index] != null)
  145. {
  146. continue;
  147. }
  148. component.AmmoSlots[index] = uid;
  149. Containers.Insert(uid, component.AmmoContainer);
  150. SetChamber(index, component, uid);
  151. Audio.PlayPredicted(component.SoundInsert, revolverUid, user);
  152. Popup(Loc.GetString("gun-revolver-insert"), revolverUid, user);
  153. UpdateRevolverAppearance(revolverUid, component);
  154. UpdateAmmoCount(revolverUid);
  155. Dirty(revolverUid, component);
  156. return true;
  157. }
  158. Popup(Loc.GetString("gun-revolver-full"), revolverUid, user);
  159. return false;
  160. }
  161. private void SetChamber(int index, RevolverAmmoProviderComponent component, EntityUid uid)
  162. {
  163. if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge) && cartridge.Spent)
  164. {
  165. component.Chambers[index] = false;
  166. return;
  167. }
  168. component.Chambers[index] = true;
  169. }
  170. private void OnRevolverVerbs(EntityUid uid, RevolverAmmoProviderComponent component, GetVerbsEvent<AlternativeVerb> args)
  171. {
  172. if (!args.CanAccess || !args.CanInteract || args.Hands == null)
  173. return;
  174. args.Verbs.Add(new AlternativeVerb()
  175. {
  176. Text = Loc.GetString("gun-revolver-empty"),
  177. Disabled = !AnyRevolverCartridges(component),
  178. Act = () => EmptyRevolver(uid, component, args.User),
  179. Priority = 1
  180. });
  181. args.Verbs.Add(new AlternativeVerb()
  182. {
  183. Text = Loc.GetString("gun-revolver-spin"),
  184. // Category = VerbCategory.G,
  185. Act = () => SpinRevolver(uid, component, args.User)
  186. });
  187. }
  188. private bool AnyRevolverCartridges(RevolverAmmoProviderComponent component)
  189. {
  190. for (var i = 0; i < component.Capacity; i++)
  191. {
  192. if (component.Chambers[i] != null ||
  193. component.AmmoSlots[i] != null)
  194. {
  195. return true;
  196. }
  197. }
  198. return false;
  199. }
  200. private int GetRevolverCount(RevolverAmmoProviderComponent component)
  201. {
  202. var count = 0;
  203. for (var i = 0; i < component.Capacity; i++)
  204. {
  205. if (component.Chambers[i] != null ||
  206. component.AmmoSlots[i] != null)
  207. {
  208. count++;
  209. }
  210. }
  211. return count;
  212. }
  213. [PublicAPI]
  214. private int GetRevolverUnspentCount(RevolverAmmoProviderComponent component)
  215. {
  216. var count = 0;
  217. for (var i = 0; i < component.Capacity; i++)
  218. {
  219. var chamber = component.Chambers[i];
  220. if (chamber == true)
  221. {
  222. count++;
  223. continue;
  224. }
  225. var ammo = component.AmmoSlots[i];
  226. if (TryComp<CartridgeAmmoComponent>(ammo, out var cartridge) && !cartridge.Spent)
  227. {
  228. count++;
  229. }
  230. }
  231. return count;
  232. }
  233. public void EmptyRevolver(EntityUid revolverUid, RevolverAmmoProviderComponent component, EntityUid? user = null)
  234. {
  235. var mapCoordinates = TransformSystem.GetMapCoordinates(revolverUid);
  236. var anyEmpty = false;
  237. for (var i = 0; i < component.Capacity; i++)
  238. {
  239. var chamber = component.Chambers[i];
  240. var slot = component.AmmoSlots[i];
  241. if (slot == null)
  242. {
  243. if (chamber == null)
  244. continue;
  245. // Too lazy to make a new method don't sue me.
  246. if (!_netManager.IsClient)
  247. {
  248. var uid = Spawn(component.FillPrototype, mapCoordinates);
  249. if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
  250. SetCartridgeSpent(uid, cartridge, !(bool) chamber);
  251. EjectCartridge(uid);
  252. }
  253. component.Chambers[i] = null;
  254. anyEmpty = true;
  255. }
  256. else
  257. {
  258. component.AmmoSlots[i] = null;
  259. Containers.Remove(slot.Value, component.AmmoContainer);
  260. component.Chambers[i] = null;
  261. if (!_netManager.IsClient)
  262. EjectCartridge(slot.Value);
  263. anyEmpty = true;
  264. }
  265. }
  266. if (anyEmpty)
  267. {
  268. Audio.PlayPredicted(component.SoundEject, revolverUid, user);
  269. UpdateAmmoCount(revolverUid, prediction: false);
  270. UpdateRevolverAppearance(revolverUid, component);
  271. Dirty(revolverUid, component);
  272. }
  273. }
  274. private void UpdateRevolverAppearance(EntityUid uid, RevolverAmmoProviderComponent component)
  275. {
  276. if (!TryComp<AppearanceComponent>(uid, out var appearance))
  277. return;
  278. var count = GetRevolverCount(component);
  279. Appearance.SetData(uid, AmmoVisuals.HasAmmo, count != 0, appearance);
  280. Appearance.SetData(uid, AmmoVisuals.AmmoCount, count, appearance);
  281. Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
  282. }
  283. protected virtual void SpinRevolver(EntityUid revolverUid, RevolverAmmoProviderComponent component, EntityUid? user = null)
  284. {
  285. Audio.PlayPredicted(component.SoundSpin, revolverUid, user);
  286. Popup(Loc.GetString("gun-revolver-spun"), revolverUid, user);
  287. }
  288. private void OnRevolverTakeAmmo(EntityUid uid, RevolverAmmoProviderComponent component, TakeAmmoEvent args)
  289. {
  290. var currentIndex = component.CurrentIndex;
  291. Cycle(component, args.Shots);
  292. // Revolvers provide the bullets themselves rather than the cartridges so they stay in the revolver.
  293. for (var i = 0; i < args.Shots; i++)
  294. {
  295. var index = (currentIndex + i) % component.Capacity;
  296. var chamber = component.Chambers[index];
  297. EntityUid? ent = null;
  298. // Get contained entity if it exists.
  299. if (component.AmmoSlots[index] != null)
  300. {
  301. ent = component.AmmoSlots[index]!;
  302. component.Chambers[index] = false;
  303. }
  304. // Try to spawn a round if it's available.
  305. else if (chamber != null)
  306. {
  307. if (chamber == true)
  308. {
  309. // Pretend it's always been there.
  310. ent = Spawn(component.FillPrototype, args.Coordinates);
  311. if (!_netManager.IsClient)
  312. {
  313. component.AmmoSlots[index] = ent;
  314. Containers.Insert(ent.Value, component.AmmoContainer);
  315. }
  316. component.Chambers[index] = false;
  317. }
  318. }
  319. // Chamber empty or spent
  320. if (ent == null)
  321. continue;
  322. if (TryComp<CartridgeAmmoComponent>(ent, out var cartridge))
  323. {
  324. if (cartridge.Spent)
  325. continue;
  326. // Mark cartridge as spent and if it's caseless delete from the chamber slot.
  327. SetCartridgeSpent(ent.Value, cartridge, true);
  328. var spawned = Spawn(cartridge.Prototype, args.Coordinates);
  329. args.Ammo.Add((spawned, EnsureComp<AmmoComponent>(spawned)));
  330. if (cartridge.DeleteOnSpawn)
  331. {
  332. component.AmmoSlots[index] = null;
  333. component.Chambers[index] = null;
  334. }
  335. }
  336. else
  337. {
  338. component.AmmoSlots[index] = null;
  339. component.Chambers[index] = null;
  340. args.Ammo.Add((ent.Value, EnsureComp<AmmoComponent>(ent.Value)));
  341. }
  342. // Delete the cartridge entity on client
  343. if (_netManager.IsClient)
  344. {
  345. QueueDel(ent);
  346. }
  347. }
  348. UpdateAmmoCount(uid, prediction: false);
  349. UpdateRevolverAppearance(uid, component);
  350. Dirty(uid, component);
  351. }
  352. private void Cycle(RevolverAmmoProviderComponent component, int count = 1)
  353. {
  354. component.CurrentIndex = (component.CurrentIndex + count) % component.Capacity;
  355. }
  356. private void OnRevolverInit(EntityUid uid, RevolverAmmoProviderComponent component, ComponentInit args)
  357. {
  358. component.AmmoContainer = Containers.EnsureContainer<Container>(uid, RevolverContainer);
  359. component.AmmoSlots.EnsureCapacity(component.Capacity);
  360. var remainder = component.Capacity - component.AmmoSlots.Count;
  361. for (var i = 0; i < remainder; i++)
  362. {
  363. component.AmmoSlots.Add(null);
  364. }
  365. component.Chambers = new bool?[component.Capacity];
  366. if (component.FillPrototype != null)
  367. {
  368. for (var i = 0; i < component.Capacity; i++)
  369. {
  370. if (component.AmmoSlots[i] != null)
  371. {
  372. component.Chambers[i] = null;
  373. continue;
  374. }
  375. component.Chambers[i] = true;
  376. }
  377. }
  378. DebugTools.Assert(component.AmmoSlots.Count == component.Capacity);
  379. }
  380. [Serializable, NetSerializable]
  381. protected sealed class RevolverAmmoProviderComponentState : ComponentState
  382. {
  383. public int CurrentIndex;
  384. public List<NetEntity?> AmmoSlots = default!;
  385. public bool?[] Chambers = default!;
  386. }
  387. public sealed class RevolverSpinEvent : EntityEventArgs
  388. {
  389. }
  390. }