FactionUIController.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. using Content.Client.Gameplay;
  2. using Content.Client.UserInterface.Controls;
  3. using Content.Client.UserInterface.Systems.Faction.Windows;
  4. using Content.Shared.Input;
  5. using JetBrains.Annotations;
  6. using Robust.Client.Player;
  7. using Robust.Client.UserInterface.Controllers;
  8. using Robust.Client.UserInterface.Controls;
  9. using Robust.Shared.Input.Binding;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Utility;
  12. using static Robust.Client.UserInterface.Controls.BaseButton;
  13. using Robust.Client.Console;
  14. using Content.Shared.Civ14.CivFactions;
  15. using Content.Client.Popups;
  16. using Content.Shared.Popups;
  17. using System.Linq;
  18. using System.Text;
  19. using Robust.Shared.Network;
  20. using Robust.Shared.GameObjects;
  21. using Robust.Shared.Player; // Required for ICommonSession
  22. using Content.Client.UserInterface.Systems.MenuBar.Widgets;
  23. namespace Content.Client.UserInterface.Systems.Faction;
  24. [UsedImplicitly]
  25. public sealed class FactionUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
  26. {
  27. [Dependency] private readonly IEntityManager _ent = default!;
  28. [Dependency] private readonly ILogManager _logMan = default!;
  29. [Dependency] private readonly IPlayerManager _player = default!;
  30. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  31. [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
  32. [Dependency] private readonly IClientNetManager _netManager = default!;
  33. private PopupSystem? _popupSystem; // Make nullable
  34. private ISawmill _sawmill = default!;
  35. private FactionWindow? _window; // Make nullable
  36. // Ensure the namespace and class name are correct for GameTopMenuBar
  37. private MenuButton? FactionButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.FactionButton;
  38. /// <summary>
  39. /// Performs initial setup for the faction UI controller, including subscribing to relevant network events and configuring logging.
  40. /// </summary>
  41. public override void Initialize()
  42. {
  43. base.Initialize();
  44. // Try to get PopupSystem. If this fails (e.g., due to initialization order issues),
  45. // _popupSystem will remain null. We'll attempt to resolve it lazily later if needed,
  46. // or handle its absence. This avoids a startup crash if EntitySystemManager is problematic.
  47. SubscribeNetworkEvent<FactionInviteOfferEvent>(OnFactionInviteOffer);
  48. SubscribeNetworkEvent<PlayerFactionStatusChangedEvent>(OnPlayerFactionStatusChanged);
  49. _sawmill = _logMan.GetSawmill("faction");
  50. }
  51. /// <summary>
  52. /// Handles entering the gameplay state by creating and configuring the faction window, wiring up UI events, registering keybinds, and loading the faction menu button.
  53. /// </summary>
  54. public void OnStateEntered(GameplayState state)
  55. {
  56. // _window should be null here if OnStateExited cleaned up properly
  57. // DebugTools.Assert(_window == null); // Keep this assertion
  58. _sawmill.Debug("FactionUIController entering GameplayState.");
  59. // Create the window instance
  60. _window = UIManager.CreateWindow<FactionWindow>();
  61. LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
  62. _sawmill.Debug("FactionWindow created.");
  63. // Wire up window events
  64. _window.OnClose += DeactivateButton;
  65. _window.OnOpen += ActivateButton;
  66. _window.OnListFactionsPressed += HandleListFactionsPressed;
  67. _window.OnCreateFactionPressed += HandleCreateFactionPressed;
  68. _window.OnLeaveFactionPressed += HandleLeaveFactionPressed;
  69. _window.OnInvitePlayerPressed += HandleInvitePlayerPressed;
  70. _sawmill.Debug("FactionWindow events subscribed.");
  71. // Bind the key function
  72. CommandBinds.Builder
  73. .Bind(ContentKeyFunctions.OpenFactionsMenu,
  74. // Use the simpler FromDelegate overload
  75. InputCmdHandler.FromDelegate(session => // Takes the session argument
  76. {
  77. // Perform the 'canExecute' check manually inside the action
  78. if (_window != null)
  79. {
  80. ToggleWindow();
  81. }
  82. else
  83. {
  84. // Log an error if trying to toggle a null window via keybind
  85. _sawmill.Error("Tried to toggle FactionWindow via keybind, but it was null.");
  86. }
  87. }))
  88. .Register<FactionUIController>(); // Registering ties it to this controller's lifecycle
  89. _sawmill.Debug("Faction keybind registered.");
  90. // *** Ensure LoadButton is still called ***
  91. LoadButton();
  92. }
  93. /// <summary>
  94. /// Cleans up faction UI elements and event handlers when exiting the gameplay state.
  95. /// </summary>
  96. /// <param name="state">The gameplay state being exited.</param>
  97. public void OnStateExited(GameplayState state)
  98. {
  99. _sawmill.Debug("FactionUIController exiting GameplayState.");
  100. if (_window != null)
  101. {
  102. _sawmill.Debug("Cleaning up FactionWindow.");
  103. _window.OnClose -= DeactivateButton;
  104. _window.OnOpen -= ActivateButton;
  105. _window.OnListFactionsPressed -= HandleListFactionsPressed;
  106. _window.OnCreateFactionPressed -= HandleCreateFactionPressed;
  107. _window.OnLeaveFactionPressed -= HandleLeaveFactionPressed;
  108. _window.OnInvitePlayerPressed -= HandleInvitePlayerPressed;
  109. // Ensure window is closed before disposing
  110. if (_window.IsOpen)
  111. _window.Close();
  112. _window.Dispose();
  113. _window = null; // Set to null after disposal
  114. }
  115. // Unregister keybind
  116. CommandBinds.Unregister<FactionUIController>();
  117. _sawmill.Debug("Faction keybind unregistered.");
  118. // *** ADD THIS LINE ***
  119. // Unload the button hookup
  120. UnloadButton();
  121. }
  122. /// <summary>
  123. /// Retrieves the first available <see cref="CivFactionsComponent"/> found in the game state, or null if none exist.
  124. /// </summary>
  125. /// <returns>The first discovered <see cref="CivFactionsComponent"/>, or null if not found.</returns>
  126. /// <remarks>
  127. /// Logs detailed information about all found instances and warns if multiple or none are present. The returned component may not be the authoritative instance if multiple exist.
  128. /// </remarks>
  129. private CivFactionsComponent? GetCivFactionsComponent()
  130. {
  131. var query = _ent.EntityQueryEnumerator<CivFactionsComponent, MetaDataComponent>();
  132. CivFactionsComponent? firstComp = null;
  133. EntityUid? firstOwner = null;
  134. MetaDataComponent? firstMeta = null;
  135. int instanceCount = 0;
  136. _sawmill.Debug("Starting search for CivFactionsComponent instances...");
  137. while (query.MoveNext(out var ownerUid, out var comp, out var metadata))
  138. {
  139. instanceCount++;
  140. if (firstComp == null) // Store the first one found
  141. {
  142. firstComp = comp;
  143. firstOwner = ownerUid;
  144. firstMeta = metadata;
  145. }
  146. // Log details for every instance found
  147. var listIsNull = comp.FactionList == null;
  148. var listCount = listIsNull ? "N/A (list is null)" : comp.FactionList!.Count.ToString();
  149. _sawmill.Debug($"Discovered CivFactionsComponent on entity {ownerUid} (Name: '{metadata.EntityName}', Prototype: '{metadata.EntityPrototype?.ID ?? "N/A"}'). FactionList is null: {listIsNull}, FactionList count: {listCount}.");
  150. }
  151. if (instanceCount > 1 && firstOwner.HasValue && firstMeta != null)
  152. {
  153. _sawmill.Warning($"Found {instanceCount} instances of CivFactionsComponent. Using the first one found on entity {firstOwner.Value} (Name: '{firstMeta.EntityName}'). This might not be the authoritative instance.");
  154. }
  155. else if (instanceCount == 0)
  156. {
  157. _sawmill.Warning("Could not find any CivFactionsComponent in the game state.");
  158. }
  159. return firstComp; // Return the first component found, or null if none
  160. }
  161. /// <summary>
  162. /// Handles a faction invite offer by notifying the player with a popup and chat messages containing instructions to accept the invite.
  163. /// </summary>
  164. private void OnFactionInviteOffer(FactionInviteOfferEvent msg, EntitySessionEventArgs args)
  165. {
  166. _sawmill.Info($"Received faction invite from {msg.InviterName} for faction '{msg.FactionName}'.");
  167. // Improved feedback using a clickable popup or chat message
  168. var message = $"{msg.InviterName} invited you to join faction '{msg.FactionName}'.";
  169. var acceptCommand = $"/acceptfactioninvite \"{msg.FactionName}\""; // Use quotes for names with spaces
  170. // You could use a more interactive popup system if available,
  171. // but for now, let's add the command hint to the popup/chat.
  172. var fullMessage = $"{message}\nType '{acceptCommand}' in chat to accept.";
  173. if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity)
  174. {
  175. // Only use _popupSystem if it was successfully retrieved
  176. _popupSystem?.PopupEntity(fullMessage, playerEntity, PopupType.Medium);
  177. }
  178. else
  179. {
  180. // Fallback if player entity isn't available or popup system isn't
  181. _popupSystem?.PopupCursor(fullMessage); // Show on cursor if possible
  182. _sawmill.Warning($"Could not show faction invite popup on entity (player entity not found or PopupSystem unavailable). Falling back to cursor popup if PopupSystem exists. Message: {fullMessage}");
  183. }
  184. // As a very robust fallback, also send to chat, as popups can sometimes be missed or problematic.
  185. _consoleHost.ExecuteCommand($"say \"{message}\"");
  186. _consoleHost.ExecuteCommand($"echo \"To accept, type: {acceptCommand}\""); // Echo to self for easy copy/paste
  187. }
  188. /// <summary>
  189. /// Handles updates to the player's faction status, refreshing the faction window UI and updating the player's faction component if necessary.
  190. /// </summary>
  191. private void OnPlayerFactionStatusChanged(PlayerFactionStatusChangedEvent msg, EntitySessionEventArgs args)
  192. {
  193. _sawmill.Info($"Received PlayerFactionStatusChangedEvent: IsInFaction={msg.IsInFaction}, FactionName='{msg.FactionName ?? "null"}'.");
  194. if (_window != null && _window.IsOpen)
  195. {
  196. _sawmill.Debug("PlayerFactionStatusChangedEvent received while window is open. Updating window state and faction list.");
  197. // Update the main view (InFactionView/NotInFactionView) based on the event
  198. _window.UpdateState(msg.IsInFaction, msg.FactionName);
  199. // Then, explicitly refresh the faction list display based on the latest component data
  200. // This ensures the list content (member counts, etc.) is also up-to-date.
  201. HandleListFactionsPressed();
  202. if (msg.IsInFaction == true && msg.FactionName != null)
  203. {
  204. if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
  205. {
  206. _sawmill.Debug($"Updating faction component for player entity: {_player.LocalEntity}");
  207. factionComp.SetFaction(msg.FactionName);
  208. _sawmill.Debug($"Faction name set to '{msg.FactionName}'({factionComp.FactionName}) in CivFactionComponent.");
  209. }
  210. }
  211. }
  212. else
  213. {
  214. _sawmill.Debug("PlayerFactionStatusChangedEvent received, but window is not open or null. No immediate UI refresh.");
  215. }
  216. }
  217. /// <summary>
  218. /// Determines whether the local player is a member of any faction and returns the faction name if applicable.
  219. /// </summary>
  220. /// <returns>
  221. /// A tuple containing a boolean indicating membership status and the name of the faction if the player is a member; otherwise, null.
  222. /// </returns>
  223. private (bool IsInFaction, string? FactionName) GetPlayerFactionStatus()
  224. {
  225. var localPlayerSession = _player.LocalSession;
  226. if (localPlayerSession == null)
  227. {
  228. _sawmill.Warning("LocalPlayerSession is null for faction status check.");
  229. return (false, null);
  230. }
  231. // Get the NetUserId and convert it to string for comparison.
  232. // NetUserId.ToString() produces a consistent lowercase GUID string.
  233. var localPlayerNetId = localPlayerSession.UserId;
  234. var localPlayerIdString = localPlayerNetId.ToString();
  235. _sawmill.Debug($"GetPlayerFactionStatus: Attempting to find player ID string '{localPlayerIdString}' in factions.");
  236. // Retrieve the global factions component
  237. var factionsComp = GetCivFactionsComponent();
  238. if (factionsComp == null)
  239. {
  240. _sawmill.Debug("CivFactionsComponent not found for faction status check.");
  241. return (false, null); // Not necessarily an error if the component doesn't exist yet
  242. }
  243. if (factionsComp.FactionList == null)
  244. {
  245. _sawmill.Warning("CivFactionsComponent.FactionList is null.");
  246. return (false, null);
  247. }
  248. // Iterate through each faction to check for the player's membership
  249. foreach (var faction in factionsComp.FactionList)
  250. {
  251. // Log the current faction being checked and its members for detailed debugging
  252. var membersString = faction.FactionMembers == null ? "null" : $"[{string.Join(", ", faction.FactionMembers)}]";
  253. _sawmill.Debug($"GetPlayerFactionStatus: Checking faction '{faction.FactionName ?? "Unnamed Faction"}'. Members: {membersString}");
  254. if (faction.FactionMembers != null && faction.FactionMembers.Contains(localPlayerIdString))
  255. {
  256. _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' FOUND in faction '{faction.FactionName}'.");
  257. return (true, faction.FactionName);
  258. }
  259. else if (faction.FactionMembers == null)
  260. {
  261. _sawmill.Debug($"GetPlayerFactionStatus: Faction '{faction.FactionName ?? "Unnamed Faction"}' has a null FactionMembers list.");
  262. }
  263. else
  264. {
  265. // This branch means FactionMembers is not null, but does not contain localPlayerIdString
  266. _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' NOT found in faction '{faction.FactionName ?? "Unnamed Faction"}'.");
  267. }
  268. }
  269. _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' was not found in any faction after checking all.");
  270. return (false, null);
  271. }
  272. /// <summary>
  273. /// Displays a list of all existing factions and their member counts in the faction window.
  274. /// </summary>
  275. /// <remarks>
  276. /// If no faction data is available or no factions exist, an appropriate message is shown instead.
  277. /// </remarks>
  278. private void HandleListFactionsPressed()
  279. {
  280. _sawmill.Info("List Factions button pressed. Querying local state...");
  281. if (_window == null)
  282. {
  283. _sawmill.Error("HandleListFactionsPressed called but _window is null!");
  284. return;
  285. }
  286. var factionsComp = GetCivFactionsComponent();
  287. if (factionsComp == null || factionsComp.FactionList == null) // Check FactionList null
  288. {
  289. _window.UpdateFactionList("Faction data not available.");
  290. _sawmill.Warning("Faction data unavailable for listing.");
  291. return;
  292. }
  293. if (factionsComp.FactionList.Count == 0)
  294. {
  295. _window.UpdateFactionList("No factions currently exist.");
  296. _sawmill.Info("Displayed empty faction list.");
  297. return;
  298. }
  299. var listBuilder = new StringBuilder();
  300. // OrderBy requires System.Linq
  301. foreach (var faction in factionsComp.FactionList.OrderBy(f => f.FactionName))
  302. {
  303. // Added detailed logging to inspect faction members state
  304. _sawmill.Debug($"Inspecting faction for UI list: '{faction.FactionName ?? "Unnamed Faction"}'");
  305. if (faction.FactionMembers == null)
  306. {
  307. _sawmill.Debug($" - FactionMembers list is null.");
  308. }
  309. else
  310. {
  311. _sawmill.Debug($" - FactionMembers.Count = {faction.FactionMembers.Count}");
  312. if (faction.FactionMembers.Count > 0)
  313. _sawmill.Debug($" - Members: [{string.Join(", ", faction.FactionMembers)}]");
  314. }
  315. // *** FIX: Construct the string first, then append ***
  316. string factionLine = $"{faction.FactionName ?? "Unnamed Faction"}: {faction.FactionMembers?.Count ?? 0} members";
  317. listBuilder.AppendLine(factionLine); // Use the AppendLine(string) overload
  318. }
  319. _window.UpdateFactionList(listBuilder.ToString());
  320. _sawmill.Info($"Displayed faction list with {factionsComp.FactionList.Count} factions.");
  321. }
  322. /// <summary>
  323. /// Handles the creation of a new faction based on user input, performing client-side validation and sending a creation request to the server.
  324. /// </summary>
  325. private void HandleCreateFactionPressed()
  326. {
  327. if (_window == null)
  328. {
  329. _sawmill.Error("Attempted to create faction, but FactionWindow is null!");
  330. return;
  331. }
  332. // Get the desired name from the window's input field
  333. // Assumes FactionWindow has a public property 'FactionNameInputText'
  334. var desiredName = _window.FactionNameInputText.Trim();
  335. // --- Client-side validation (Good practice) ---
  336. if (string.IsNullOrWhiteSpace(desiredName))
  337. {
  338. _sawmill.Warning("Create Faction pressed with empty name.");
  339. var errorMsg = "Faction name cannot be empty.";
  340. if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity && _popupSystem != null)
  341. _popupSystem.PopupEntity(errorMsg, playerEntity, PopupType.SmallCaution);
  342. else // Fallback to cursor popup or console if entity/popupsystem is unavailable
  343. _popupSystem?.PopupCursor(errorMsg); // Use null-conditional
  344. return;
  345. }
  346. // Check length (sync this limit with server-side validation in CivFactionsSystem)
  347. const int maxNameLength = 32;
  348. if (desiredName.Length > maxNameLength)
  349. {
  350. _sawmill.Warning($"Create Faction pressed with name too long: {desiredName}");
  351. var msg = $"Faction name is too long (max {maxNameLength} characters).";
  352. if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity && _popupSystem != null)
  353. _popupSystem.PopupEntity(msg, playerEntity, PopupType.SmallCaution);
  354. else // Fallback
  355. _popupSystem?.PopupCursor(msg); // Use null-conditional
  356. return;
  357. }
  358. // --- End Client-side validation ---
  359. _sawmill.Info($"Requesting to create faction with name: '{desiredName}'");
  360. // FIX: Call the constructor directly with the required argument
  361. var createEvent = new CreateFactionRequestEvent(desiredName);
  362. // Send the event to the server
  363. _ent.RaisePredictiveEvent(createEvent);
  364. _sawmill.Debug("Sent CreateFactionRequestEvent to server.");
  365. // Optional: Clear the input field in the UI after sending the request
  366. _window.ClearFactionNameInput(); // Assumes FactionWindow has this method
  367. // Attempt to refresh the window state immediately.
  368. // This relies on the server processing the request and the client receiving
  369. // the updated CivFactionsComponent relatively quickly.
  370. // A more robust solution might involve a server confirmation event or a short delay.
  371. // RefreshFactionWindowState(); // Removed: UI will update via PlayerFactionStatusChangedEvent
  372. //probably need to check if the name is being used or not
  373. if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
  374. {
  375. if (factionComp.FactionName == "")
  376. {
  377. _sawmill.Debug($"Setting faction name to '{desiredName}' in CivFactionComponent.");
  378. factionComp.SetFaction(desiredName);
  379. }
  380. }
  381. }
  382. /// <summary>
  383. /// Handles the action when the player chooses to leave their current faction, sending a leave request to the server and clearing the local faction component.
  384. /// </summary>
  385. private void HandleLeaveFactionPressed()
  386. {
  387. _sawmill.Info("Leave Faction button pressed.");
  388. var leaveEvent = new LeaveFactionRequestEvent();
  389. // Raise the network event to send it to the server
  390. _ent.RaisePredictiveEvent(leaveEvent); // Use RaisePredictiveEvent for client-initiated actions
  391. _sawmill.Info("Sent LeaveFactionRequestEvent to server.");
  392. if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
  393. {
  394. factionComp.SetFaction("");
  395. }
  396. // Attempt to refresh the window state immediately.
  397. // RefreshFactionWindowState(); // Removed: UI will update via PlayerFactionStatusChangedEvent
  398. }
  399. /// <summary>
  400. /// Handles the invite player action from the faction window, validating input, searching for the player by name, and sending an invite request to the server.
  401. /// </summary>
  402. private void HandleInvitePlayerPressed()
  403. {
  404. _sawmill.Debug("Invite Player button pressed.");
  405. if (_window == null)
  406. {
  407. _sawmill.Error("Attempted to invite player, but FactionWindow is null!");
  408. return;
  409. }
  410. // Get the target player's name from the window's input field
  411. var targetPlayerName = _window.InvitePlayerNameInputText.Trim();
  412. if (string.IsNullOrWhiteSpace(targetPlayerName))
  413. {
  414. _sawmill.Debug("Invite player: Name field is empty.");
  415. _popupSystem?.PopupCursor("Player name cannot be empty.", PopupType.SmallCaution);
  416. return;
  417. }
  418. _sawmill.Info($"Attempting to invite player: '{targetPlayerName}'");
  419. // Find the player session by name (case-insensitive search)
  420. var targetSession = _player.Sessions.FirstOrDefault( // Sessions are ICommonSession on client
  421. s => s.Name.Equals(targetPlayerName, StringComparison.OrdinalIgnoreCase) // Name is available on ICommonSession
  422. );
  423. if (targetSession == null)
  424. {
  425. var notFoundMsg = $"Player '{targetPlayerName}' not found.";
  426. _sawmill.Warning(notFoundMsg);
  427. _popupSystem?.PopupCursor(notFoundMsg, PopupType.SmallCaution);
  428. return;
  429. }
  430. // Player found, get their NetUserId. UserId is available on ICommonSession.
  431. NetUserId targetUserId = targetSession.UserId; // Correctly accesses UserId from ICommonSession
  432. // Create the event
  433. var inviteEvent = new InviteFactionRequestEvent(targetUserId);
  434. // Send the event to the server
  435. _ent.RaisePredictiveEvent(inviteEvent);
  436. _sawmill.Info($"Sent InviteFactionRequestEvent for target player '{targetPlayerName}' (ID: {targetUserId}) to server.");
  437. _popupSystem?.PopupCursor($"Invite sent to {targetPlayerName}.", PopupType.Small);
  438. _window.ClearInvitePlayerNameInput(); // Clear the input field
  439. }
  440. /// <summary>
  441. /// Refreshes the faction window's main view (in/not in faction) and the faction list.
  442. /// Call this after actions that might change the player's faction status or the list of factions.
  443. /// <summary>
  444. /// Updates the faction window UI to reflect the player's current faction status and the latest faction list.
  445. /// </summary>
  446. private void RefreshFactionWindowState()
  447. {
  448. if (_window == null)
  449. {
  450. _sawmill.Warning("RefreshFactionWindowState called but _window is null!");
  451. return;
  452. }
  453. if (!_window.IsOpen) // No need to refresh if not open
  454. {
  455. _sawmill.Debug("RefreshFactionWindowState called but window is not open.");
  456. return;
  457. }
  458. _sawmill.Debug("Refreshing faction window state...");
  459. var (isInFaction, factionName) = GetPlayerFactionStatus();
  460. _window.UpdateState(isInFaction, factionName); // This updates NotInFactionView vs InFactionView
  461. HandleListFactionsPressed(); // This updates the FactionListLabel
  462. _sawmill.Debug("Faction window state refreshed.");
  463. }
  464. /// <summary>
  465. /// Unsubscribes the faction button from its pressed event and deactivates its pressed state.
  466. /// </summary>
  467. public void UnloadButton()
  468. {
  469. if (FactionButton == null)
  470. {
  471. _sawmill.Debug("FactionButton is null during UnloadButton, cannot unsubscribe.");
  472. return;
  473. }
  474. FactionButton.OnPressed -= FactionButtonPressed;
  475. _sawmill.Debug("Unsubscribed from FactionButton.OnPressed.");
  476. // Also deactivate button state if window is closed during unload
  477. DeactivateButton();
  478. }
  479. /// <summary>
  480. /// Subscribes to the faction button's press event and synchronises its pressed state with the faction window's visibility.
  481. /// </summary>
  482. public void LoadButton()
  483. {
  484. if (FactionButton == null)
  485. {
  486. // This might happen if the UI loads slightly out of order.
  487. // Could add a small delay/retry or ensure GameTopMenuBar is ready first.
  488. _sawmill.Warning("FactionButton is null during LoadButton. Button press won't work yet.");
  489. return; // Can't subscribe if button doesn't exist yet
  490. }
  491. // Prevent double-subscribing
  492. FactionButton.OnPressed -= FactionButtonPressed;
  493. FactionButton.OnPressed += FactionButtonPressed;
  494. _sawmill.Debug("Subscribed to FactionButton.OnPressed.");
  495. // Update button state based on current window state
  496. if (_window != null)
  497. FactionButton.Pressed = _window.IsOpen;
  498. }
  499. /// <summary>
  500. /// Sets the faction button's pressed state to inactive if the button exists.
  501. /// </summary>
  502. private void DeactivateButton()
  503. {
  504. if (FactionButton == null) return;
  505. FactionButton.Pressed = false;
  506. _sawmill.Debug("Deactivated FactionButton visual state.");
  507. }
  508. /// <summary>
  509. /// Sets the faction button's pressed state to active if the button exists.
  510. /// </summary>
  511. private void ActivateButton()
  512. {
  513. if (FactionButton == null) return;
  514. FactionButton.Pressed = true;
  515. _sawmill.Debug("Activated FactionButton visual state.");
  516. }
  517. /// <summary>
  518. /// Closes the faction window if it exists and is currently open.
  519. /// </summary>
  520. private void CloseWindow()
  521. {
  522. if (_window == null)
  523. {
  524. _sawmill.Warning("CloseWindow called but _window is null.");
  525. return;
  526. }
  527. if (_window.IsOpen) // Only close if open
  528. {
  529. _window.Close();
  530. _sawmill.Debug("FactionWindow closed via CloseWindow().");
  531. }
  532. }
  533. /// <summary>
  534. /// Handles the faction button press event by toggling the faction window's visibility.
  535. /// </summary>
  536. private void FactionButtonPressed(ButtonEventArgs args)
  537. {
  538. _sawmill.Debug("FactionButton pressed, calling ToggleWindow.");
  539. ToggleWindow();
  540. }
  541. /// <summary>
  542. /// Toggles the visibility of the faction management window, updating its state and synchronising the faction button's visual state.
  543. /// </summary>
  544. private void ToggleWindow()
  545. {
  546. _sawmill.Debug($"ToggleWindow called. Window is null: {_window == null}");
  547. if (_window == null)
  548. {
  549. _sawmill.Error("Attempted to toggle FactionWindow, but it's null!");
  550. // Maybe try to recreate it? Or just log the error.
  551. // For now, just return to prevent NullReferenceException
  552. return;
  553. }
  554. _sawmill.Debug($"Window IsOpen: {_window.IsOpen}");
  555. if (_window.IsOpen)
  556. {
  557. CloseWindow();
  558. }
  559. else
  560. {
  561. _sawmill.Debug("Opening FactionWindow...");
  562. // Get current status *before* opening
  563. var (isInFaction, factionName) = GetPlayerFactionStatus();
  564. _sawmill.Debug($"Player status: IsInFaction={isInFaction}, FactionName={factionName ?? "null"}");
  565. // Update the window state (which view to show)
  566. _window.UpdateState(isInFaction, factionName);
  567. _sawmill.Debug("FactionWindow state updated.");
  568. // Open the window
  569. _window.Open();
  570. _sawmill.Debug("FactionWindow opened.");
  571. // Optionally, refresh the list immediately on open
  572. // This ensures the faction list is populated when the window is first opened.
  573. HandleListFactionsPressed();
  574. }
  575. // Update button visual state AFTER toggling
  576. // Use null-conditional operator just in case FactionButton became null somehow
  577. // FactionButton?.SetClickPressed(_window?.IsOpen ?? false); // SetClickPressed might not be what you want, .Pressed is usually better for toggle state
  578. if (FactionButton != null)
  579. {
  580. FactionButton.Pressed = _window.IsOpen;
  581. _sawmill.Debug($"FactionButton visual state set to Pressed: {FactionButton.Pressed}");
  582. }
  583. }
  584. }