FactionUIController.cs 33 KB

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