LobbyUIController.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. using System.Linq;
  2. using Content.Client.Guidebook;
  3. using Content.Client.Humanoid;
  4. using Content.Client.Inventory;
  5. using Content.Client.Lobby.UI;
  6. using Content.Client.Players.PlayTimeTracking;
  7. using Content.Client.Station;
  8. using Content.Shared.CCVar;
  9. using Content.Shared.Clothing;
  10. using Content.Shared.GameTicking;
  11. using Content.Shared.Humanoid;
  12. using Content.Shared.Humanoid.Markings;
  13. using Content.Shared.Humanoid.Prototypes;
  14. using Content.Shared.Preferences;
  15. using Content.Shared.Preferences.Loadouts;
  16. using Content.Shared.Roles;
  17. using Content.Shared.Traits;
  18. using Robust.Client.Player;
  19. using Robust.Client.ResourceManagement;
  20. using Robust.Client.State;
  21. using Robust.Client.UserInterface;
  22. using Robust.Client.UserInterface.Controllers;
  23. using Robust.Shared.Configuration;
  24. using Robust.Shared.Map;
  25. using Robust.Shared.Prototypes;
  26. using Robust.Shared.Utility;
  27. namespace Content.Client.Lobby;
  28. public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
  29. {
  30. [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
  31. [Dependency] private readonly IConfigurationManager _configurationManager = default!;
  32. [Dependency] private readonly IFileDialogManager _dialogManager = default!;
  33. [Dependency] private readonly ILogManager _logManager = default!;
  34. [Dependency] private readonly IPlayerManager _playerManager = default!;
  35. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  36. [Dependency] private readonly IResourceCache _resourceCache = default!;
  37. [Dependency] private readonly IStateManager _stateManager = default!;
  38. [Dependency] private readonly JobRequirementsManager _requirements = default!;
  39. [Dependency] private readonly MarkingManager _markings = default!;
  40. [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
  41. [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
  42. [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
  43. [UISystemDependency] private readonly GuidebookSystem _guide = default!;
  44. private CharacterSetupGui? _characterSetup;
  45. private HumanoidProfileEditor? _profileEditor;
  46. private CharacterSetupGuiSavePanel? _savePanel;
  47. /// <summary>
  48. /// This is the characher preview panel in the chat. This should only update if their character updates.
  49. /// </summary>
  50. private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview();
  51. /// <summary>
  52. /// This is the modified profile currently being edited.
  53. /// </summary>
  54. private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile;
  55. private int? EditedSlot => _profileEditor?.CharacterSlot;
  56. public override void Initialize()
  57. {
  58. base.Initialize();
  59. _prototypeManager.PrototypesReloaded += OnProtoReload;
  60. _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
  61. _requirements.Updated += OnRequirementsUpdated;
  62. _configurationManager.OnValueChanged(CCVars.FlavorText, args =>
  63. {
  64. _profileEditor?.RefreshFlavorText();
  65. });
  66. _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
  67. _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
  68. }
  69. private LobbyCharacterPreviewPanel? GetLobbyPreview()
  70. {
  71. if (_stateManager.CurrentState is LobbyState lobby)
  72. {
  73. return lobby.Lobby?.CharacterPreview;
  74. }
  75. return null;
  76. }
  77. private void OnRequirementsUpdated()
  78. {
  79. if (_profileEditor != null)
  80. {
  81. _profileEditor.RefreshAntags();
  82. _profileEditor.RefreshJobs();
  83. }
  84. }
  85. private void OnProtoReload(PrototypesReloadedEventArgs obj)
  86. {
  87. if (_profileEditor != null)
  88. {
  89. if (obj.WasModified<AntagPrototype>())
  90. {
  91. _profileEditor.RefreshAntags();
  92. }
  93. if (obj.WasModified<JobPrototype>() ||
  94. obj.WasModified<DepartmentPrototype>())
  95. {
  96. _profileEditor.RefreshJobs();
  97. }
  98. if (obj.WasModified<LoadoutPrototype>() ||
  99. obj.WasModified<LoadoutGroupPrototype>() ||
  100. obj.WasModified<RoleLoadoutPrototype>())
  101. {
  102. _profileEditor.RefreshLoadouts();
  103. }
  104. if (obj.WasModified<SpeciesPrototype>())
  105. {
  106. _profileEditor.RefreshSpecies();
  107. }
  108. if (obj.WasModified<TraitPrototype>())
  109. {
  110. _profileEditor.RefreshTraits();
  111. }
  112. }
  113. }
  114. private void PreferencesDataLoaded()
  115. {
  116. PreviewPanel?.SetLoaded(true);
  117. if (_stateManager.CurrentState is not LobbyState)
  118. return;
  119. ReloadCharacterSetup();
  120. }
  121. public void OnStateEntered(LobbyState state)
  122. {
  123. PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded);
  124. ReloadCharacterSetup();
  125. }
  126. public void OnStateExited(LobbyState state)
  127. {
  128. PreviewPanel?.SetLoaded(false);
  129. _profileEditor?.Dispose();
  130. _characterSetup?.Dispose();
  131. _characterSetup = null;
  132. _profileEditor = null;
  133. }
  134. /// <summary>
  135. /// Reloads every single character setup control.
  136. /// </summary>
  137. public void ReloadCharacterSetup()
  138. {
  139. RefreshLobbyPreview();
  140. var (characterGui, profileEditor) = EnsureGui();
  141. characterGui.ReloadCharacterPickers();
  142. profileEditor.SetProfile(
  143. (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
  144. _preferencesManager.Preferences?.SelectedCharacterIndex);
  145. }
  146. /// <summary>
  147. /// Refreshes the character preview in the lobby chat.
  148. /// </summary>
  149. private void RefreshLobbyPreview()
  150. {
  151. if (PreviewPanel == null)
  152. return;
  153. // Get selected character, load it, then set it
  154. var character = _preferencesManager.Preferences?.SelectedCharacter;
  155. if (character is not HumanoidCharacterProfile humanoid)
  156. {
  157. PreviewPanel.SetSprite(EntityUid.Invalid);
  158. PreviewPanel.SetSummaryText(string.Empty);
  159. return;
  160. }
  161. var dummy = LoadProfileEntity(humanoid, null, true);
  162. PreviewPanel.SetSprite(dummy);
  163. PreviewPanel.SetSummaryText(humanoid.Summary);
  164. }
  165. private void RefreshProfileEditor()
  166. {
  167. _profileEditor?.RefreshAntags();
  168. _profileEditor?.RefreshJobs();
  169. _profileEditor?.RefreshLoadouts();
  170. }
  171. private void SaveProfile()
  172. {
  173. DebugTools.Assert(EditedProfile != null);
  174. if (EditedProfile == null || EditedSlot == null)
  175. return;
  176. var selected = _preferencesManager.Preferences?.SelectedCharacterIndex;
  177. if (selected == null)
  178. return;
  179. _preferencesManager.UpdateCharacter(EditedProfile, EditedSlot.Value);
  180. ReloadCharacterSetup();
  181. }
  182. private void CloseProfileEditor()
  183. {
  184. if (_profileEditor == null)
  185. return;
  186. _profileEditor.SetProfile(null, null);
  187. _profileEditor.Visible = false;
  188. if (_stateManager.CurrentState is LobbyState lobbyGui)
  189. {
  190. lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default);
  191. }
  192. }
  193. private void OpenSavePanel()
  194. {
  195. if (_savePanel is { IsOpen: true })
  196. return;
  197. _savePanel = new CharacterSetupGuiSavePanel();
  198. _savePanel.SaveButton.OnPressed += _ =>
  199. {
  200. SaveProfile();
  201. _savePanel.Close();
  202. CloseProfileEditor();
  203. };
  204. _savePanel.NoSaveButton.OnPressed += _ =>
  205. {
  206. _savePanel.Close();
  207. CloseProfileEditor();
  208. };
  209. _savePanel.OpenCentered();
  210. }
  211. private (CharacterSetupGui, HumanoidProfileEditor) EnsureGui()
  212. {
  213. if (_characterSetup != null && _profileEditor != null)
  214. {
  215. _characterSetup.Visible = true;
  216. _profileEditor.Visible = true;
  217. return (_characterSetup, _profileEditor);
  218. }
  219. _profileEditor = new HumanoidProfileEditor(
  220. _preferencesManager,
  221. _configurationManager,
  222. EntityManager,
  223. _dialogManager,
  224. _logManager,
  225. _playerManager,
  226. _prototypeManager,
  227. _resourceCache,
  228. _requirements,
  229. _markings);
  230. _profileEditor.OnOpenGuidebook += _guide.OpenHelp;
  231. _characterSetup = new CharacterSetupGui(_profileEditor);
  232. _characterSetup.CloseButton.OnPressed += _ =>
  233. {
  234. // Open the save panel if we have unsaved changes.
  235. if (_profileEditor.Profile != null && _profileEditor.IsDirty)
  236. {
  237. OpenSavePanel();
  238. return;
  239. }
  240. // Reset sliders etc.
  241. CloseProfileEditor();
  242. };
  243. _profileEditor.Save += SaveProfile;
  244. _characterSetup.SelectCharacter += args =>
  245. {
  246. _preferencesManager.SelectCharacter(args);
  247. ReloadCharacterSetup();
  248. };
  249. _characterSetup.DeleteCharacter += args =>
  250. {
  251. _preferencesManager.DeleteCharacter(args);
  252. // Reload everything
  253. if (EditedSlot == args)
  254. {
  255. ReloadCharacterSetup();
  256. }
  257. else
  258. {
  259. // Only need to reload character pickers
  260. _characterSetup?.ReloadCharacterPickers();
  261. }
  262. };
  263. if (_stateManager.CurrentState is LobbyState lobby)
  264. {
  265. lobby.Lobby?.CharacterSetupState.AddChild(_characterSetup);
  266. }
  267. return (_characterSetup, _profileEditor);
  268. }
  269. #region Helpers
  270. /// <summary>
  271. /// Applies the highest priority job's clothes to the dummy.
  272. /// </summary>
  273. public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile)
  274. {
  275. var job = jobProto ?? GetPreferredJob(profile);
  276. GiveDummyJobClothes(dummy, profile, job);
  277. if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
  278. {
  279. var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, profile.Species, EntityManager, _prototypeManager);
  280. GiveDummyLoadout(dummy, loadout);
  281. }
  282. }
  283. /// <summary>
  284. /// Gets the highest priority job for the profile.
  285. /// </summary>
  286. public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
  287. {
  288. var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
  289. // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
  290. return _prototypeManager.Index<JobPrototype>(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob);
  291. }
  292. public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
  293. {
  294. if (roleLoadout == null)
  295. return;
  296. foreach (var group in roleLoadout.SelectedLoadouts.Values)
  297. {
  298. foreach (var loadout in group)
  299. {
  300. if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
  301. continue;
  302. _spawn.EquipStartingGear(uid, loadoutProto);
  303. }
  304. }
  305. }
  306. /// <summary>
  307. /// Applies the specified job's clothes to the dummy.
  308. /// </summary>
  309. public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
  310. {
  311. if (!_inventory.TryGetSlots(dummy, out var slots))
  312. return;
  313. // Apply loadout
  314. if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
  315. {
  316. foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
  317. {
  318. foreach (var loadout in loadouts)
  319. {
  320. if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
  321. continue;
  322. // TODO: Need some way to apply starting gear to an entity and replace existing stuff coz holy fucking shit dude.
  323. foreach (var slot in slots)
  324. {
  325. // Try startinggear first
  326. if (_prototypeManager.TryIndex(loadoutProto.StartingGear, out var loadoutGear))
  327. {
  328. var itemType = ((IEquipmentLoadout) loadoutGear).GetGear(slot.Name);
  329. if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
  330. {
  331. EntityManager.DeleteEntity(unequippedItem.Value);
  332. }
  333. if (itemType != string.Empty)
  334. {
  335. var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
  336. _inventory.TryEquip(dummy, item, slot.Name, true, true);
  337. }
  338. }
  339. else
  340. {
  341. var itemType = ((IEquipmentLoadout) loadoutProto).GetGear(slot.Name);
  342. if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
  343. {
  344. EntityManager.DeleteEntity(unequippedItem.Value);
  345. }
  346. if (itemType != string.Empty)
  347. {
  348. var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
  349. _inventory.TryEquip(dummy, item, slot.Name, true, true);
  350. }
  351. }
  352. }
  353. }
  354. }
  355. }
  356. if (!_prototypeManager.TryIndex(job.StartingGear, out var gear))
  357. return;
  358. foreach (var slot in slots)
  359. {
  360. var itemType = ((IEquipmentLoadout) gear).GetGear(slot.Name);
  361. if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
  362. {
  363. EntityManager.DeleteEntity(unequippedItem.Value);
  364. }
  365. if (itemType != string.Empty)
  366. {
  367. var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
  368. _inventory.TryEquip(dummy, item, slot.Name, true, true);
  369. }
  370. }
  371. }
  372. /// <summary>
  373. /// Loads the profile onto a dummy entity.
  374. /// </summary>
  375. public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes)
  376. {
  377. EntityUid dummyEnt;
  378. EntProtoId? previewEntity = null;
  379. if (humanoid != null && jobClothes)
  380. {
  381. job ??= GetPreferredJob(humanoid);
  382. previewEntity = job.JobPreviewEntity ?? (EntProtoId?)job?.JobEntity;
  383. }
  384. if (previewEntity != null)
  385. {
  386. // Special type like borg or AI, do not spawn a human just spawn the entity.
  387. dummyEnt = EntityManager.SpawnEntity(previewEntity, MapCoordinates.Nullspace);
  388. return dummyEnt;
  389. }
  390. else if (humanoid is not null)
  391. {
  392. var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
  393. dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
  394. }
  395. else
  396. {
  397. dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
  398. }
  399. _humanoid.LoadProfile(dummyEnt, humanoid);
  400. if (humanoid != null && jobClothes)
  401. {
  402. DebugTools.Assert(job != null);
  403. GiveDummyJobClothes(dummyEnt, humanoid, job);
  404. if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
  405. {
  406. var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, humanoid.Species, EntityManager, _prototypeManager);
  407. GiveDummyLoadout(dummyEnt, loadout);
  408. }
  409. }
  410. return dummyEnt;
  411. }
  412. #endregion
  413. }