1
0

RoleLoadout.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Shared.Humanoid.Prototypes;
  4. using Content.Shared.Random;
  5. using Robust.Shared.Collections;
  6. using Robust.Shared.Player;
  7. using Robust.Shared.Prototypes;
  8. using Robust.Shared.Serialization;
  9. using Robust.Shared.Utility;
  10. namespace Content.Shared.Preferences.Loadouts;
  11. /// <summary>
  12. /// Contains all of the selected data for a role's loadout.
  13. /// </summary>
  14. [Serializable, NetSerializable, DataDefinition]
  15. public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
  16. {
  17. [DataField]
  18. public ProtoId<RoleLoadoutPrototype> Role;
  19. [DataField]
  20. public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
  21. /// <summary>
  22. /// Loadout specific name.
  23. /// </summary>
  24. public string? EntityName;
  25. /*
  26. * Loadout-specific data used for validation.
  27. */
  28. public int? Points;
  29. public RoleLoadout(ProtoId<RoleLoadoutPrototype> role)
  30. {
  31. Role = role;
  32. }
  33. public RoleLoadout Clone()
  34. {
  35. var weh = new RoleLoadout(Role);
  36. foreach (var selected in SelectedLoadouts)
  37. {
  38. weh.SelectedLoadouts.Add(selected.Key, new List<Loadout>(selected.Value));
  39. }
  40. weh.EntityName = EntityName;
  41. return weh;
  42. }
  43. /// <summary>
  44. /// Ensures all prototypes exist and effects can be applied.
  45. /// </summary>
  46. public void EnsureValid(HumanoidCharacterProfile profile, ICommonSession session, IDependencyCollection collection)
  47. {
  48. var groupRemove = new ValueList<string>();
  49. var protoManager = collection.Resolve<IPrototypeManager>();
  50. if (!protoManager.TryIndex(Role, out var roleProto))
  51. {
  52. EntityName = null;
  53. SelectedLoadouts.Clear();
  54. return;
  55. }
  56. // Remove name not allowed.
  57. if (!roleProto.CanCustomizeName)
  58. {
  59. EntityName = null;
  60. }
  61. // Validate name length
  62. // TODO: Probably allow regex to be supplied?
  63. if (EntityName != null)
  64. {
  65. var name = EntityName.Trim();
  66. if (name.Length > HumanoidCharacterProfile.MaxNameLength)
  67. {
  68. EntityName = name[..HumanoidCharacterProfile.MaxNameLength];
  69. }
  70. if (name.Length == 0)
  71. {
  72. EntityName = null;
  73. }
  74. }
  75. // In some instances we might not have picked up a new group for existing data.
  76. foreach (var groupProto in roleProto.Groups)
  77. {
  78. if (SelectedLoadouts.ContainsKey(groupProto))
  79. continue;
  80. // Data will get set below.
  81. SelectedLoadouts[groupProto] = new List<Loadout>();
  82. }
  83. // Reset points to recalculate.
  84. Points = roleProto.Points;
  85. foreach (var (group, groupLoadouts) in SelectedLoadouts)
  86. {
  87. // Check the group is even valid for this role.
  88. if (!roleProto.Groups.Contains(group))
  89. {
  90. groupRemove.Add(group);
  91. continue;
  92. }
  93. // Dump if Group doesn't exist
  94. if (!protoManager.TryIndex(group, out var groupProto))
  95. {
  96. groupRemove.Add(group);
  97. continue;
  98. }
  99. var loadouts = groupLoadouts[..Math.Min(groupLoadouts.Count, groupProto.MaxLimit)];
  100. // Validate first
  101. for (var i = loadouts.Count - 1; i >= 0; i--)
  102. {
  103. var loadout = loadouts[i];
  104. // Old prototype or otherwise invalid.
  105. if (!protoManager.TryIndex(loadout.Prototype, out var loadoutProto))
  106. {
  107. loadouts.RemoveAt(i);
  108. continue;
  109. }
  110. // Malicious client maybe, check the group even has it.
  111. if (!groupProto.Loadouts.Contains(loadout.Prototype))
  112. {
  113. loadouts.RemoveAt(i);
  114. continue;
  115. }
  116. // Validate the loadout can be applied (e.g. points).
  117. if (!IsValid(profile, session, loadout.Prototype, collection, out _))
  118. {
  119. loadouts.RemoveAt(i);
  120. continue;
  121. }
  122. Apply(loadoutProto);
  123. }
  124. // Apply defaults if required
  125. // Technically it's possible for someone to game themselves into loadouts they shouldn't have
  126. // If you put invalid ones first but that's your fault for not using sensible defaults
  127. if (loadouts.Count < groupProto.MinLimit)
  128. {
  129. foreach (var protoId in groupProto.Loadouts)
  130. {
  131. if (loadouts.Count >= groupProto.MinLimit)
  132. break;
  133. if (!protoManager.TryIndex(protoId, out var loadoutProto))
  134. continue;
  135. var defaultLoadout = new Loadout()
  136. {
  137. Prototype = loadoutProto.ID,
  138. };
  139. if (loadouts.Contains(defaultLoadout))
  140. continue;
  141. // Not valid so don't default to it anyway.
  142. if (!IsValid(profile, session, defaultLoadout.Prototype, collection, out _))
  143. continue;
  144. loadouts.Add(defaultLoadout);
  145. Apply(loadoutProto);
  146. }
  147. }
  148. SelectedLoadouts[group] = loadouts;
  149. }
  150. foreach (var value in groupRemove)
  151. {
  152. SelectedLoadouts.Remove(value);
  153. }
  154. }
  155. private void Apply(LoadoutPrototype loadoutProto)
  156. {
  157. foreach (var effect in loadoutProto.Effects)
  158. {
  159. effect.Apply(this);
  160. }
  161. }
  162. /// <summary>
  163. /// Resets the selected loadouts to default if no data is present.
  164. /// </summary>
  165. public void SetDefault(HumanoidCharacterProfile? profile, ICommonSession? session, IPrototypeManager protoManager, bool force = false)
  166. {
  167. if (profile == null)
  168. return;
  169. if (force)
  170. SelectedLoadouts.Clear();
  171. var collection = IoCManager.Instance!;
  172. var roleProto = protoManager.Index(Role);
  173. for (var i = roleProto.Groups.Count - 1; i >= 0; i--)
  174. {
  175. var group = roleProto.Groups[i];
  176. if (!protoManager.TryIndex(group, out var groupProto))
  177. continue;
  178. if (SelectedLoadouts.ContainsKey(group))
  179. continue;
  180. var loadouts = new List<Loadout>();
  181. SelectedLoadouts[group] = loadouts;
  182. if (groupProto.MinLimit > 0)
  183. {
  184. // Apply any loadouts we can.
  185. foreach (var protoId in groupProto.Loadouts)
  186. {
  187. // Reached the limit, time to stop
  188. if (loadouts.Count >= groupProto.MinLimit)
  189. break;
  190. if (!protoManager.TryIndex(protoId, out var loadoutProto))
  191. continue;
  192. var defaultLoadout = new Loadout()
  193. {
  194. Prototype = loadoutProto.ID,
  195. };
  196. // Not valid so don't default to it anyway.
  197. if (!IsValid(profile, session, defaultLoadout.Prototype, collection, out _))
  198. continue;
  199. loadouts.Add(defaultLoadout);
  200. Apply(loadoutProto);
  201. }
  202. }
  203. }
  204. }
  205. /// <summary>
  206. /// Returns whether a loadout is valid or not.
  207. /// </summary>
  208. public bool IsValid(HumanoidCharacterProfile profile, ICommonSession? session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
  209. {
  210. reason = null;
  211. var protoManager = collection.Resolve<IPrototypeManager>();
  212. if (!protoManager.TryIndex(loadout, out var loadoutProto))
  213. {
  214. // Uhh
  215. reason = FormattedMessage.FromMarkupOrThrow("");
  216. return false;
  217. }
  218. if (!protoManager.HasIndex(Role))
  219. {
  220. reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
  221. return false;
  222. }
  223. var valid = true;
  224. foreach (var effect in loadoutProto.Effects)
  225. {
  226. valid = valid && effect.Validate(profile, this, session, collection, out reason);
  227. }
  228. return valid;
  229. }
  230. /// <summary>
  231. /// Applies the specified loadout to this group.
  232. /// </summary>
  233. public bool AddLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
  234. {
  235. var groupLoadouts = SelectedLoadouts[selectedGroup];
  236. // Need to unselect existing ones if we're at or above limit
  237. var limit = Math.Max(0, groupLoadouts.Count + 1 - protoManager.Index(selectedGroup).MaxLimit);
  238. for (var i = 0; i < groupLoadouts.Count; i++)
  239. {
  240. var loadout = groupLoadouts[i];
  241. if (loadout.Prototype != selectedLoadout)
  242. {
  243. // Remove any other loadouts that might push it above the limit.
  244. if (limit > 0)
  245. {
  246. limit--;
  247. groupLoadouts.RemoveAt(i);
  248. i--;
  249. }
  250. continue;
  251. }
  252. DebugTools.Assert(false);
  253. return false;
  254. }
  255. groupLoadouts.Add(new Loadout()
  256. {
  257. Prototype = selectedLoadout,
  258. });
  259. return true;
  260. }
  261. /// <summary>
  262. /// Removed the specified loadout from this group.
  263. /// </summary>
  264. public bool RemoveLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
  265. {
  266. // Although this may bring us below minimum we'll let EnsureValid handle it.
  267. var groupLoadouts = SelectedLoadouts[selectedGroup];
  268. for (var i = 0; i < groupLoadouts.Count; i++)
  269. {
  270. var loadout = groupLoadouts[i];
  271. if (loadout.Prototype != selectedLoadout)
  272. continue;
  273. groupLoadouts.RemoveAt(i);
  274. return true;
  275. }
  276. return false;
  277. }
  278. public bool Equals(RoleLoadout? other)
  279. {
  280. if (ReferenceEquals(null, other)) return false;
  281. if (ReferenceEquals(this, other)) return true;
  282. if (!Role.Equals(other.Role) ||
  283. SelectedLoadouts.Count != other.SelectedLoadouts.Count ||
  284. Points != other.Points ||
  285. EntityName != other.EntityName)
  286. {
  287. return false;
  288. }
  289. // Tried using SequenceEqual but it stinky so.
  290. foreach (var (key, value) in SelectedLoadouts)
  291. {
  292. if (!other.SelectedLoadouts.TryGetValue(key, out var otherValue) ||
  293. !otherValue.SequenceEqual(value))
  294. {
  295. return false;
  296. }
  297. }
  298. return true;
  299. }
  300. public override bool Equals(object? obj)
  301. {
  302. return ReferenceEquals(this, obj) || obj is RoleLoadout other && Equals(other);
  303. }
  304. public override int GetHashCode()
  305. {
  306. return HashCode.Combine(Role, SelectedLoadouts, Points);
  307. }
  308. }