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