浏览代码

Factions and Research (#192)

* adds research component

* faction framework

* Update from master (#142)

* Update entities_clothing_suit.yml (#131)

this is my first pull req so theres probably at least 10 bugs even though im only editing data 😭

* feat: changing tiles (#132)

Co-authored-by: Alex Sampaio <alexsampaio.contato@gmail.com>

* wall fix

* Update predators.yml (#133)

no more village wiping 🔥

* Clothes weights (#137)

* backpacks weight change

* hands weight change

* Update entities_clothing_suit.yml (#140)

* Update entities_clothing_suit.yml

self explanatory

* Update entities_clothing_suit.yml

fixed the typo

* Factions top game bar button (#141)

* Update ContentContexts.cs

* Update KeyRebindTab.xaml.cs

* Update GameTopMenuBar.xaml

* Update ContentKeyFunctions.cs

* Update keybinds.yml

* Update game-hud.ftl

---------

Co-authored-by: fruitnoodle <147153046+fruitnoodle@users.noreply.github.com>
Co-authored-by: Alex Sampaio <alexsampaio.contato@gmail.com>
Co-authored-by: Papiditel <mrharved@gmail.com>

* faction icon

* update from master (#155)

* Update entities_clothing_suit.yml (#131)

this is my first pull req so theres probably at least 10 bugs even though im only editing data 😭

* feat: changing tiles (#132)

Co-authored-by: Alex Sampaio <alexsampaio.contato@gmail.com>

* wall fix

* Update predators.yml (#133)

no more village wiping 🔥

* Clothes weights (#137)

* backpacks weight change

* hands weight change

* Update entities_clothing_suit.yml (#140)

* Update entities_clothing_suit.yml

self explanatory

* Update entities_clothing_suit.yml

fixed the typo

* Factions top game bar button (#141)

* Update ContentContexts.cs

* Update KeyRebindTab.xaml.cs

* Update GameTopMenuBar.xaml

* Update ContentKeyFunctions.cs

* Update keybinds.yml

* Update game-hud.ftl

* workflow update

* Weight stuff (#150)

* Updated default weight for species

Lowered default weight for humans because every organ and body part has weight which basically makes naked person without anything weight 87kg

* Update base.yml

Lowered default weight for humans because every organ and body part has weight which basically makes naked person without anything weight 87kg

* Lowered belt weight

* Lowered weight clothingeyes

* lowered weight base_clothinghead.yml

* lowered base_clothingmask.yml

* Update towel.yml

* Update base_clothingneck.yml

* Update base_clothingouter.yml

* Update wintercoats.yml

* Update base_clothingshoes.yml

* Update base_clothing.yml

* Update base_clothing.yml

again

* dough, compostables (#151)

* dough, compostables

* bone helmet fix

* hammer and brazier fixes, fence gates not bumpable

* roast potatoes

* changelog

* Playlist fixes (#154)

* dough, compostables

* bone helmet fix

* hammer and brazier fixes, fence gates not bumpable

* roast potatoes

* changelog

* update workflow

* destroyable tiki torches, stackable seeds and healing herbs

* more burnables

* seed stacks fix

* update client fix

* no tags on abstract

* tag fix

* tag fix, again

* workflows

---------

Co-authored-by: fruitnoodle <147153046+fruitnoodle@users.noreply.github.com>
Co-authored-by: Alex Sampaio <alexsampaio.contato@gmail.com>
Co-authored-by: Papiditel <mrharved@gmail.com>

* factionbutton fix

* wip faction window

* fixing some errors

* more WIP faction stuff (which doesnt woek yet)

* more wip factions

* faction UI displays now

* adding faction names

* Squashed commit of the following:

commit fa5a31a1fcbadff2f8da767d4b9e0ac13fb2c112
Author: Taislin <taislin@civ13.com>
Date:   Sat May 10 14:02:37 2025 +0100

    Update upload-client.yml

commit 0248c61c1e8d4153e9d6c7b61a07050bb95d88a2
Author: Taislin <taislin@civ13.com>
Date:   Sat May 10 13:53:55 2025 +0100

    Fixes upload_client.yml

commit 7553df7be1abcd3efcc22e66961f09e72b1e001e
Author: Taislin <taislin@civ13.com>
Date:   Sat May 10 00:42:44 2025 +0100

    TDM update 2 (#190)

    * k/d tracker

    * suit, helmet weights, spear range, respawn timers

    * fixing waterskins, some strings

    * fixes point capturing for criticals

    * faster rotting

    * barricade directionals, craftable recipes

    * yaml fix

    * map tweaks

    * added flags ported from civ13

    * capture timer, map tweaks

    * goob's wideswing tweaks, stamina damage

    * 📝 Add docstrings to `tdm` (#191)

    Docstrings generation was requested by @taislin.

    * https://github.com/Civ13/Civ14/pull/190#issuecomment-2864561046

    The following files were modified:

    * `Content.Server/GameTicking/Rules/CaptureAreaSystem.cs`
    * `Content.Shared/Damage/Events/TakeStaminaDamageEvent.cs`
    * `Content.Shared/Damage/Systems/StaminaSystem.cs`
    * `Content.Shared/Ensnaring/SharedEnsnareableSystem.cs`
    * `Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs`
    * `Content.Shared/_Shitcode/Weapons/DodgeWideswing/DodgeWideswingSystem.cs`

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

    * crenelated walls should not be climbable, stamina crit message

    ---------

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

commit 21d0ce4a756125362ec231f986ba79515f4f2912
Author: Taislin <taislin@civ13.com>
Date:   Wed May 7 19:56:33 2025 +0100

    TDM update 1 (#187)

    * k/d tracker

    * suit, helmet weights, spear range, respawn timers

    * fixing waterskins, some strings

    * fixes point capturing for criticals

    * faster rotting

    * barricade directionals, craftable recipes

    * yaml fix

    * map tweaks

commit edc2331315a168dbba6efe5d009f067b04057302
Author: Taislin <taislin@civ13.com>
Date:   Mon May 5 18:16:39 2025 +0100

    TDM (#183)

    * tdm jobs init

    * more job stuff

    * missing inhand sprites

    * adding other jobs

    * sprite fixes, swordsmen and pikemen

    * shield icon fix

    * crenelated walls

    * cobblestone floors

    * loadouts

    * barricading

    * tilefix

    * barricades working

    * map tweaks

    * medieval walls

    * lanterns, stove

    * mapping

    * respawn fixes, localised names, return to lobby button for ghosts, icons for soldiers

    * shield and armour weights, faction component

    * faction icons, WIP grace wall and capture area systems

    * framework for captureareas and gracewall

    * capturable and grace wall areas

    * gracewall fix, capturing mechanic working, tabards and gambeson

    * castle gates, randomised loadouts

    * faction spawnpoints, longquan sprite fix (this time for real), more mapping

    * fixes mining with swords, arrow blindness

    * attribution fix

    * yaml fixes

* menu bar icon, fixing guidebook

* disabling factions on non-nomads

* faction creation

* ready for testing

* 📝 Add docstrings to `factions_research` (#197)

Docstrings generation was requested by @taislin.

* https://github.com/Civ13/Civ14/pull/192#issuecomment-2868856583

The following files were modified:

* `Content.Client/Construction/UI/ConstructionMenuPresenter.cs`
* `Content.Client/Examine/ExamineSystem.cs`
* `Content.Client/UserInterface/Systems/Faction/FactionUIController.cs`
* `Content.Client/UserInterface/Systems/Faction/Windows/FactionWindow.xaml.cs`
* `Content.Server/Chat/Systems/ChatSystem.cs`
* `Content.Server/Civ14/CivFactions/CivFactionsSystem.cs`
* `Content.Shared/Civ14/CivFactions/CivFactionComponent.cs`
* `Content.Shared/Civ14/CivFactions/CivFactionsEvents.cs`
* `Content.Shared/Civ14/CivFactions/FactionExamineSystem.cs`
* `Content.Shared/Civ14/CivResearch/CivResearchSystem.cs`
* `Content.Shared/Examine/ExamineSystemShared.cs`
* `Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs`
* `Content.Shared/_Stalker/Weight/SharedWeightExamineInfoSystem.cs`
* `mapGeneration.py`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* coderabbit suggestions

---------

Co-authored-by: fruitnoodle <147153046+fruitnoodle@users.noreply.github.com>
Co-authored-by: Alex Sampaio <alexsampaio.contato@gmail.com>
Co-authored-by: Papiditel <mrharved@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Taislin 6 月之前
父节点
当前提交
d341bb197f
共有 33 个文件被更改,包括 1792 次插入94 次删除
  1. 0 1
      .github/workflows/rsi-diff.yml
  2. 26 6
      Content.Client/Construction/UI/ConstructionMenuPresenter.cs
  3. 20 5
      Content.Client/Examine/ExamineSystem.cs
  4. 666 0
      Content.Client/UserInterface/Systems/Faction/FactionUIController.cs
  5. 87 0
      Content.Client/UserInterface/Systems/Faction/Windows/FactionWindow.xaml
  6. 111 0
      Content.Client/UserInterface/Systems/Faction/Windows/FactionWindow.xaml.cs
  7. 34 45
      Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
  8. 9 5
      Content.Server/Chat/Systems/ChatSystem.cs
  9. 350 0
      Content.Server/Civ14/CivFactions/CivFactionsSystem.cs
  10. 12 0
      Content.Server/GameTicking/Rules/Components/FactionRuleComponent.cs
  11. 24 0
      Content.Shared/Civ14/CivFactions/CivFactionComponent.cs
  12. 25 0
      Content.Shared/Civ14/CivFactions/CivFactionsComponent.cs
  13. 134 0
      Content.Shared/Civ14/CivFactions/CivFactionsEvents.cs
  14. 0 0
      Content.Shared/Civ14/CivFactions/CivFactionsSystem.cs
  15. 46 0
      Content.Shared/Civ14/CivFactions/FactionData.cs
  16. 50 0
      Content.Shared/Civ14/CivFactions/FactionExamineSystem.cs
  17. 51 0
      Content.Shared/Civ14/CivResearch/CivResearchComponent.cs
  18. 69 0
      Content.Shared/Civ14/CivResearch/CivResearchSystem.cs
  19. 36 11
      Content.Shared/Examine/ExamineSystemShared.cs
  20. 3 5
      Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
  21. 12 4
      Content.Shared/_Stalker/Weight/SharedWeightExamineInfoSystem.cs
  22. 1 0
      Resources/Changelog/Changelog.yml
  23. 3 3
      Resources/ConfigPresets/Build/development.toml
  24. 2 1
      Resources/Maps/civ/nomads_classic.yml
  25. 3 3
      Resources/Prototypes/Civ14/Entities/Objects/Guns/entities_bullets.yml
  26. 1 0
      Resources/Prototypes/Entities/Mobs/Species/base.yml
  27. 6 0
      Resources/Prototypes/GameRules/roundstart.yml
  28. 1 1
      Resources/Prototypes/game_presets.yml
  29. 1 1
      Resources/ServerInfo/Guidebook/Nomads/nomadsguide.xml
  30. 1 0
      Resources/Textures/Civ14/Weapons/Guns/mosin.rsi/meta.json
  31. 二进制
      Resources/Textures/Interface/flag.png
  32. 1 1
      Wiki/src/guides/starter_guide.md
  33. 7 2
      mapGeneration.py

+ 0 - 1
.github/workflows/rsi-diff.yml

@@ -4,7 +4,6 @@ on:
   pull_request_target:
     paths:
       - "**.rsi/**.png"
-
 permissions:
   contents: read # Required for actions/checkout
   pull-requests: write # Required for commenting on pull requests

+ 26 - 6
Content.Client/Construction/UI/ConstructionMenuPresenter.cs

@@ -14,6 +14,8 @@
 using Robust.Shared.Enums;
 using Robust.Shared.Prototypes;
 using static Robust.Client.UserInterface.Controls.BaseButton;
+using Robust.Shared.Map;
+using Content.Shared.Civ14.CivResearch;
 
 namespace Content.Client.Construction.UI
 {
@@ -30,11 +32,12 @@ internal sealed class ConstructionMenuPresenter : IDisposable
         [Dependency] private readonly IPlacementManager _placementManager = default!;
         [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
         [Dependency] private readonly IPlayerManager _playerManager = default!;
-
+        [Dependency] private readonly IMapManager _mapManager = default!;
+        [Dependency] private readonly ILogManager _logManager = default!;
+        private ISawmill _sawmill = default!;
         private readonly IConstructionMenuView _constructionView;
         private readonly EntityWhitelistSystem _whitelistSystem;
         private readonly SpriteSystem _spriteSystem;
-
         private ConstructionSystem? _constructionSystem;
         private ConstructionPrototype? _selected;
         private List<ConstructionPrototype> _favoritedRecipes = [];
@@ -80,6 +83,8 @@ private bool WindowOpen
 
         /// <summary>
         /// Constructs a new instance of <see cref="ConstructionMenuPresenter" />.
+        /// <summary>
+        /// Initializes the ConstructionMenuPresenter, injecting dependencies, setting up the construction UI, binding event handlers, and populating initial categories and recipes.
         /// </summary>
         public ConstructionMenuPresenter()
         {
@@ -88,7 +93,6 @@ public ConstructionMenuPresenter()
             _constructionView = new ConstructionMenu();
             _whitelistSystem = _entManager.System<EntityWhitelistSystem>();
             _spriteSystem = _entManager.System<SpriteSystem>();
-
             // This is required so that if we load after the system is initialized, we can bind to it immediately
             if (_systemManager.TryGetEntitySystem<ConstructionSystem>(out var constructionSystem))
                 SystemBindingChanged(constructionSystem);
@@ -139,6 +143,9 @@ private void OnPlacementChanged(object? sender, EventArgs e)
             _constructionView.ResetPlacement();
         }
 
+        /// <summary>
+        /// Handles selection of a construction recipe from the UI, updating the selected recipe and displaying its details.
+        /// </summary>
         private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
         {
             if (item is null)
@@ -148,7 +155,7 @@ private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
                 return;
             }
 
-            _selected = (ConstructionPrototype) item.Metadata!;
+            _selected = (ConstructionPrototype)item.Metadata!;
             if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
             PopulateInfo(_selected);
         }
@@ -167,6 +174,11 @@ private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? rec
             PopulateInfo(_selected);
         }
 
+        /// <summary>
+        /// Populates the construction recipe list or grid in the UI based on the current search term and selected category, filtering recipes by visibility, player eligibility, whitelist, and research age.
+        /// </summary>
+        /// <param name="sender">The event sender (unused).</param>
+        /// <param name="args">A tuple containing the search string and selected category.</param>
         private void OnViewPopulateRecipes(object? sender, (string search, string category) args)
         {
             var (search, category) = args;
@@ -181,10 +193,18 @@ private void OnViewPopulateRecipes(object? sender, (string search, string catego
                 _selectedCategory = category;
             foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
             {
-                var CurrentAge = 1; //hardcoded for now
                 if (recipe.Hide)
                     continue;
-                if (CurrentAge < recipe.AgeMin || CurrentAge > recipe.AgeMax)
+                var currentAge = 0;
+                // Get the entity UID associated with the first map
+                var mapId = _mapManager.GetAllMapIds().FirstOrDefault();
+                var mapUid = _mapManager.GetMapEntityId(mapId);
+
+                if (_entManager.TryGetComponent<CivResearchComponent>(mapUid, out var comp))
+                {
+                    currentAge = (int)MathF.Floor(comp.ResearchLevel / 100);
+                }
+                if (currentAge < recipe.AgeMin || currentAge > recipe.AgeMax)
                     continue;
                 if (_playerManager.LocalSession == null
                 || _playerManager.LocalEntity == null

+ 20 - 5
Content.Client/Examine/ExamineSystem.cs

@@ -70,9 +70,12 @@ private void OnExaminedItemDropped(EntityUid item, ItemComponent comp, DroppedEv
                 CloseTooltip();
         }
 
+        /// <summary>
+        /// Closes the examine tooltip if the examined entity is no longer valid or the player can no longer examine it.
+        /// </summary>
         public override void Update(float frameTime)
         {
-            if (_examineTooltipOpen is not {Visible: true}) return;
+            if (_examineTooltipOpen is not { Visible: true }) return;
             if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return;
 
             if (!CanExamine(player, _examinedEntity))
@@ -123,6 +126,9 @@ private bool HandleExamine(in PointerInputCmdHandler.PointerInputCmdArgs args)
             return true;
         }
 
+        /// <summary>
+        /// Adds a basic examine verb to the entity's verb list if the user is permitted to examine the target.
+        /// </summary>
         private void AddExamineVerb(GetVerbsEvent<ExamineVerb> args)
         {
             if (!CanExamine(args.User, args.Target))
@@ -135,7 +141,7 @@ private void AddExamineVerb(GetVerbsEvent<ExamineVerb> args)
             // Center it on the entity if they use the verb instead.
             verb.Act = () => DoExamine(args.Target, false);
             verb.Text = Loc.GetString("examine-verb-name");
-            verb.Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/examine.svg.192dpi.png"));
+            verb.Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/examine.svg.192dpi.png"));
             verb.ShowOnExamineTooltip = false;
             verb.ClientExclusive = true;
             args.Verbs.Add(verb);
@@ -170,8 +176,15 @@ public override void SendExamineTooltip(EntityUid player, EntityUid target, Form
         ///     Opens the tooltip window and sets spriteview/name/etc, but does
         ///     not fill it with information. This is done when the server sends examine info/verbs,
         ///     or immediately if it's entirely clientside.
+        /// <summary>
+        /// Opens an examine tooltip popup for the specified entity, displaying its name and sprite at a calculated screen position.
         /// </summary>
-        public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true)
+        /// <param name="player">The entity performing the examination.</param>
+        /// <param name="target">The entity being examined.</param>
+        /// <param name="centeredOnCursor">If true, positions the tooltip at the mouse cursor; otherwise, uses the entity's screen position.</param>
+        /// <param name="openAtOldTooltip">If true and a previous tooltip was open, reuses its position for the new tooltip.</param>
+        /// <param name="knowTarget">If true, displays the entity's name; otherwise, shows a placeholder.</param>
+        public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor = true, bool openAtOldTooltip = true, bool knowTarget = true)
         {
             // Close any examine tooltip that might already be opened
             // Before we do that, save its position. We'll prioritize opening any new popups there if
@@ -257,8 +270,10 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso
 
         /// <summary>
         ///     Fills the examine tooltip with a message and buttons if applicable.
+        /// <summary>
+        /// Updates the examine tooltip with the provided message and adds available examine verbs as interactive buttons.
         /// </summary>
-        public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null)
+        public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs = null)
         {
             var vBox = _examineTooltipOpen?.GetChild(0).GetChild(0);
             if (vBox == null)
@@ -276,7 +291,7 @@ public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessa
                 if (string.IsNullOrWhiteSpace(text))
                     continue;
 
-                var richLabel = new RichTextLabel() { Margin = new Thickness(4, 4, 0, 4)};
+                var richLabel = new RichTextLabel() { Margin = new Thickness(4, 4, 0, 4) };
                 richLabel.SetMessage(message);
                 vBox.AddChild(richLabel);
                 break;

+ 666 - 0
Content.Client/UserInterface/Systems/Faction/FactionUIController.cs

@@ -0,0 +1,666 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Controls;
+using Content.Client.UserInterface.Systems.Faction.Windows;
+using Content.Shared.Input;
+using JetBrains.Annotations;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+using Robust.Client.Console;
+using Content.Shared.Civ14.CivFactions;
+using Content.Client.Popups;
+using Content.Shared.Popups;
+using System.Linq;
+using System.Text;
+using Robust.Shared.Network;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Player; // Required for ICommonSession
+using Content.Client.UserInterface.Systems.MenuBar.Widgets;
+
+
+namespace Content.Client.UserInterface.Systems.Faction;
+
+[UsedImplicitly]
+public sealed class FactionUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
+{
+    [Dependency] private readonly IEntityManager _ent = default!;
+    [Dependency] private readonly ILogManager _logMan = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
+    [Dependency] private readonly IClientNetManager _netManager = default!;
+    private PopupSystem? _popupSystem; // Make nullable
+    private ISawmill _sawmill = default!;
+    private FactionWindow? _window; // Make nullable
+    // Ensure the namespace and class name are correct for GameTopMenuBar
+    private MenuButton? FactionButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.FactionButton;
+
+    /// <summary>
+    /// Performs initial setup for the faction UI controller, including subscribing to relevant network events and configuring logging.
+    /// </summary>
+    public override void Initialize()
+    {
+        base.Initialize();
+        // Try to get PopupSystem. If this fails (e.g., due to initialization order issues),
+        // _popupSystem will remain null. We'll attempt to resolve it lazily later if needed,
+        // or handle its absence. This avoids a startup crash if EntitySystemManager is problematic.
+        SubscribeNetworkEvent<FactionInviteOfferEvent>(OnFactionInviteOffer);
+        SubscribeNetworkEvent<PlayerFactionStatusChangedEvent>(OnPlayerFactionStatusChanged);
+        _sawmill = _logMan.GetSawmill("faction");
+    }
+
+    /// <summary>
+    /// Handles entering the gameplay state by creating and configuring the faction window, wiring up UI events, registering keybinds, and loading the faction menu button.
+    /// </summary>
+    public void OnStateEntered(GameplayState state)
+    {
+        // _window should be null here if OnStateExited cleaned up properly
+        // DebugTools.Assert(_window == null); // Keep this assertion
+
+        _sawmill.Debug("FactionUIController entering GameplayState.");
+
+        // Create the window instance
+        _window = UIManager.CreateWindow<FactionWindow>();
+        LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
+        _sawmill.Debug("FactionWindow created.");
+
+        // Wire up window events
+        _window.OnClose += DeactivateButton;
+        _window.OnOpen += ActivateButton;
+        _window.OnListFactionsPressed += HandleListFactionsPressed;
+        _window.OnCreateFactionPressed += HandleCreateFactionPressed;
+        _window.OnLeaveFactionPressed += HandleLeaveFactionPressed;
+        _window.OnInvitePlayerPressed += HandleInvitePlayerPressed;
+        _sawmill.Debug("FactionWindow events subscribed.");
+
+        // Bind the key function
+        CommandBinds.Builder
+            .Bind(ContentKeyFunctions.OpenFactionsMenu,
+                // Use the simpler FromDelegate overload
+                InputCmdHandler.FromDelegate(session => // Takes the session argument
+                {
+                    // Perform the 'canExecute' check manually inside the action
+                    if (_window != null)
+                    {
+                        ToggleWindow();
+                    }
+                    else
+                    {
+                        // Log an error if trying to toggle a null window via keybind
+                        _sawmill.Error("Tried to toggle FactionWindow via keybind, but it was null.");
+                    }
+                }))
+            .Register<FactionUIController>(); // Registering ties it to this controller's lifecycle
+
+        _sawmill.Debug("Faction keybind registered.");
+
+        // *** Ensure LoadButton is still called ***
+        LoadButton();
+    }
+
+    /// <summary>
+    /// Cleans up faction UI elements and event handlers when exiting the gameplay state.
+    /// </summary>
+    /// <param name="state">The gameplay state being exited.</param>
+    public void OnStateExited(GameplayState state)
+    {
+        _sawmill.Debug("FactionUIController exiting GameplayState.");
+        if (_window != null)
+        {
+            _sawmill.Debug("Cleaning up FactionWindow.");
+            _window.OnClose -= DeactivateButton;
+            _window.OnOpen -= ActivateButton;
+            _window.OnListFactionsPressed -= HandleListFactionsPressed;
+            _window.OnCreateFactionPressed -= HandleCreateFactionPressed;
+            _window.OnLeaveFactionPressed -= HandleLeaveFactionPressed;
+            _window.OnInvitePlayerPressed -= HandleInvitePlayerPressed;
+
+            // Ensure window is closed before disposing
+            if (_window.IsOpen)
+                _window.Close();
+            _window.Dispose();
+            _window = null; // Set to null after disposal
+        }
+
+        // Unregister keybind
+        CommandBinds.Unregister<FactionUIController>();
+        _sawmill.Debug("Faction keybind unregistered.");
+
+        // *** ADD THIS LINE ***
+        // Unload the button hookup
+        UnloadButton();
+    }
+
+    /// <summary>
+    /// Retrieves the first available <see cref="CivFactionsComponent"/> found in the game state, or null if none exist.
+    /// </summary>
+    /// <returns>The first discovered <see cref="CivFactionsComponent"/>, or null if not found.</returns>
+    /// <remarks>
+    /// 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.
+    /// </remarks>
+    private CivFactionsComponent? GetCivFactionsComponent()
+    {
+        var query = _ent.EntityQueryEnumerator<CivFactionsComponent, MetaDataComponent>();
+        CivFactionsComponent? firstComp = null;
+        EntityUid? firstOwner = null;
+        MetaDataComponent? firstMeta = null;
+        int instanceCount = 0;
+
+        _sawmill.Debug("Starting search for CivFactionsComponent instances...");
+        while (query.MoveNext(out var ownerUid, out var comp, out var metadata))
+        {
+            instanceCount++;
+            if (firstComp == null) // Store the first one found
+            {
+                firstComp = comp;
+                firstOwner = ownerUid;
+                firstMeta = metadata;
+            }
+            // Log details for every instance found
+            var listIsNull = comp.FactionList == null;
+            var listCount = listIsNull ? "N/A (list is null)" : comp.FactionList!.Count.ToString();
+            _sawmill.Debug($"Discovered CivFactionsComponent on entity {ownerUid} (Name: '{metadata.EntityName}', Prototype: '{metadata.EntityPrototype?.ID ?? "N/A"}'). FactionList is null: {listIsNull}, FactionList count: {listCount}.");
+        }
+
+        if (instanceCount > 1 && firstOwner.HasValue && firstMeta != null)
+        {
+            _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.");
+        }
+        else if (instanceCount == 0)
+        {
+            _sawmill.Warning("Could not find any CivFactionsComponent in the game state.");
+        }
+        return firstComp; // Return the first component found, or null if none
+    }
+
+    /// <summary>
+    /// Handles a faction invite offer by notifying the player with a popup and chat messages containing instructions to accept the invite.
+    /// </summary>
+    private void OnFactionInviteOffer(FactionInviteOfferEvent msg, EntitySessionEventArgs args)
+    {
+        _sawmill.Info($"Received faction invite from {msg.InviterName} for faction '{msg.FactionName}'.");
+
+        // Improved feedback using a clickable popup or chat message
+        var message = $"{msg.InviterName} invited you to join faction '{msg.FactionName}'.";
+        var acceptCommand = $"/acceptfactioninvite \"{msg.FactionName}\""; // Use quotes for names with spaces
+
+        // You could use a more interactive popup system if available,
+        // but for now, let's add the command hint to the popup/chat.
+        var fullMessage = $"{message}\nType '{acceptCommand}' in chat to accept.";
+
+        if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity)
+        {
+            // Only use _popupSystem if it was successfully retrieved
+            _popupSystem?.PopupEntity(fullMessage, playerEntity, PopupType.Medium);
+        }
+        else
+        {
+            // Fallback if player entity isn't available or popup system isn't
+            _popupSystem?.PopupCursor(fullMessage); // Show on cursor if possible
+            _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}");
+        }
+        // As a very robust fallback, also send to chat, as popups can sometimes be missed or problematic.
+        _consoleHost.ExecuteCommand($"say \"{message}\"");
+        _consoleHost.ExecuteCommand($"echo \"To accept, type: {acceptCommand}\""); // Echo to self for easy copy/paste
+    }
+
+    /// <summary>
+    /// Handles updates to the player's faction status, refreshing the faction window UI and updating the player's faction component if necessary.
+    /// </summary>
+    private void OnPlayerFactionStatusChanged(PlayerFactionStatusChangedEvent msg, EntitySessionEventArgs args)
+    {
+        _sawmill.Info($"Received PlayerFactionStatusChangedEvent: IsInFaction={msg.IsInFaction}, FactionName='{msg.FactionName ?? "null"}'.");
+
+        if (_window != null && _window.IsOpen)
+        {
+            _sawmill.Debug("PlayerFactionStatusChangedEvent received while window is open. Updating window state and faction list.");
+            // Update the main view (InFactionView/NotInFactionView) based on the event
+            _window.UpdateState(msg.IsInFaction, msg.FactionName);
+            // Then, explicitly refresh the faction list display based on the latest component data
+            // This ensures the list content (member counts, etc.) is also up-to-date.
+            HandleListFactionsPressed();
+
+            if (msg.IsInFaction == true && msg.FactionName != null)
+            {
+                if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
+                {
+                    _sawmill.Debug($"Updating faction component for player entity: {_player.LocalEntity}");
+                    factionComp.SetFaction(msg.FactionName);
+                    _sawmill.Debug($"Faction name set to '{msg.FactionName}'({factionComp.FactionName}) in CivFactionComponent.");
+
+                }
+            }
+        }
+        else
+        {
+            _sawmill.Debug("PlayerFactionStatusChangedEvent received, but window is not open or null. No immediate UI refresh.");
+        }
+    }
+
+
+
+    /// <summary>
+    /// Determines whether the local player is a member of any faction and returns the faction name if applicable.
+    /// </summary>
+    /// <returns>
+    /// A tuple containing a boolean indicating membership status and the name of the faction if the player is a member; otherwise, null.
+    /// </returns>
+    private (bool IsInFaction, string? FactionName) GetPlayerFactionStatus()
+    {
+        var localPlayerSession = _player.LocalSession;
+        if (localPlayerSession == null)
+        {
+            _sawmill.Warning("LocalPlayerSession is null for faction status check.");
+            return (false, null);
+        }
+
+        // Get the NetUserId and convert it to string for comparison.
+        // NetUserId.ToString() produces a consistent lowercase GUID string.
+        var localPlayerNetId = localPlayerSession.UserId;
+        var localPlayerIdString = localPlayerNetId.ToString();
+        _sawmill.Debug($"GetPlayerFactionStatus: Attempting to find player ID string '{localPlayerIdString}' in factions.");
+
+
+        // Retrieve the global factions component
+        var factionsComp = GetCivFactionsComponent();
+        if (factionsComp == null)
+        {
+            _sawmill.Debug("CivFactionsComponent not found for faction status check.");
+            return (false, null); // Not necessarily an error if the component doesn't exist yet
+        }
+
+        if (factionsComp.FactionList == null)
+        {
+            _sawmill.Warning("CivFactionsComponent.FactionList is null.");
+            return (false, null);
+        }
+
+        // Iterate through each faction to check for the player's membership
+        foreach (var faction in factionsComp.FactionList)
+        {
+            // Log the current faction being checked and its members for detailed debugging
+            var membersString = faction.FactionMembers == null ? "null" : $"[{string.Join(", ", faction.FactionMembers)}]";
+            _sawmill.Debug($"GetPlayerFactionStatus: Checking faction '{faction.FactionName ?? "Unnamed Faction"}'. Members: {membersString}");
+
+            if (faction.FactionMembers != null && faction.FactionMembers.Contains(localPlayerIdString))
+            {
+                _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' FOUND in faction '{faction.FactionName}'.");
+                return (true, faction.FactionName);
+            }
+            else if (faction.FactionMembers == null)
+            {
+                _sawmill.Debug($"GetPlayerFactionStatus: Faction '{faction.FactionName ?? "Unnamed Faction"}' has a null FactionMembers list.");
+            }
+            else
+            {
+                // This branch means FactionMembers is not null, but does not contain localPlayerIdString
+                _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' NOT found in faction '{faction.FactionName ?? "Unnamed Faction"}'.");
+            }
+        }
+
+        _sawmill.Debug($"GetPlayerFactionStatus: Player ID string '{localPlayerIdString}' was not found in any faction after checking all.");
+        return (false, null);
+    }
+
+    /// <summary>
+    /// Displays a list of all existing factions and their member counts in the faction window.
+    /// </summary>
+    /// <remarks>
+    /// If no faction data is available or no factions exist, an appropriate message is shown instead.
+    /// </remarks>
+    private void HandleListFactionsPressed()
+    {
+        _sawmill.Info("List Factions button pressed. Querying local state...");
+
+        if (_window == null)
+        {
+            _sawmill.Error("HandleListFactionsPressed called but _window is null!");
+            return;
+        }
+
+        var factionsComp = GetCivFactionsComponent();
+        if (factionsComp == null || factionsComp.FactionList == null) // Check FactionList null
+        {
+            _window.UpdateFactionList("Faction data not available.");
+            _sawmill.Warning("Faction data unavailable for listing.");
+            return;
+        }
+
+        if (factionsComp.FactionList.Count == 0)
+        {
+            _window.UpdateFactionList("No factions currently exist.");
+            _sawmill.Info("Displayed empty faction list.");
+            return;
+        }
+
+        var listBuilder = new StringBuilder();
+        // OrderBy requires System.Linq
+        foreach (var faction in factionsComp.FactionList.OrderBy(f => f.FactionName))
+        {
+            // Added detailed logging to inspect faction members state
+            _sawmill.Debug($"Inspecting faction for UI list: '{faction.FactionName ?? "Unnamed Faction"}'");
+            if (faction.FactionMembers == null)
+            {
+                _sawmill.Debug($"  - FactionMembers list is null.");
+            }
+            else
+            {
+                _sawmill.Debug($"  - FactionMembers.Count = {faction.FactionMembers.Count}");
+                if (faction.FactionMembers.Count > 0)
+                    _sawmill.Debug($"  - Members: [{string.Join(", ", faction.FactionMembers)}]");
+            }
+
+            // *** FIX: Construct the string first, then append ***
+            string factionLine = $"{faction.FactionName ?? "Unnamed Faction"}: {faction.FactionMembers?.Count ?? 0} members";
+            listBuilder.AppendLine(factionLine); // Use the AppendLine(string) overload
+        }
+
+        _window.UpdateFactionList(listBuilder.ToString());
+        _sawmill.Info($"Displayed faction list with {factionsComp.FactionList.Count} factions.");
+    }
+    /// <summary>
+    /// Handles the creation of a new faction based on user input, performing client-side validation and sending a creation request to the server.
+    /// </summary>
+    private void HandleCreateFactionPressed()
+    {
+        if (_window == null)
+        {
+            _sawmill.Error("Attempted to create faction, but FactionWindow is null!");
+            return;
+        }
+
+        // Get the desired name from the window's input field
+        // Assumes FactionWindow has a public property 'FactionNameInputText'
+        var desiredName = _window.FactionNameInputText.Trim();
+
+        // --- Client-side validation (Good practice) ---
+        if (string.IsNullOrWhiteSpace(desiredName))
+        {
+            _sawmill.Warning("Create Faction pressed with empty name.");
+            var errorMsg = "Faction name cannot be empty.";
+            if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity && _popupSystem != null)
+                _popupSystem.PopupEntity(errorMsg, playerEntity, PopupType.SmallCaution);
+            else // Fallback to cursor popup or console if entity/popupsystem is unavailable
+                _popupSystem?.PopupCursor(errorMsg); // Use null-conditional
+            return;
+        }
+
+        // Check length (sync this limit with server-side validation in CivFactionsSystem)
+        const int maxNameLength = 32;
+        if (desiredName.Length > maxNameLength)
+        {
+            _sawmill.Warning($"Create Faction pressed with name too long: {desiredName}");
+            var msg = $"Faction name is too long (max {maxNameLength} characters).";
+            if (_player.LocalSession?.AttachedEntity is { Valid: true } playerEntity && _popupSystem != null)
+                _popupSystem.PopupEntity(msg, playerEntity, PopupType.SmallCaution);
+            else // Fallback
+                _popupSystem?.PopupCursor(msg); // Use null-conditional
+            return;
+        }
+        // --- End Client-side validation ---
+
+        _sawmill.Info($"Requesting to create faction with name: '{desiredName}'");
+
+        // FIX: Call the constructor directly with the required argument
+        var createEvent = new CreateFactionRequestEvent(desiredName);
+
+        // Send the event to the server
+        _ent.RaisePredictiveEvent(createEvent);
+
+        _sawmill.Debug("Sent CreateFactionRequestEvent to server.");
+
+        // Optional: Clear the input field in the UI after sending the request
+        _window.ClearFactionNameInput(); // Assumes FactionWindow has this method
+
+        // Attempt to refresh the window state immediately.
+        // This relies on the server processing the request and the client receiving
+        // the updated CivFactionsComponent relatively quickly.
+        // A more robust solution might involve a server confirmation event or a short delay.
+        // RefreshFactionWindowState(); // Removed: UI will update via PlayerFactionStatusChangedEvent
+
+        //probably need to check if the name is being used or not
+        if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
+        {
+            if (factionComp.FactionName == "")
+            {
+                _sawmill.Debug($"Setting faction name to '{desiredName}' in CivFactionComponent.");
+                factionComp.SetFaction(desiredName);
+            }
+        }
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    private void HandleLeaveFactionPressed()
+    {
+        _sawmill.Info("Leave Faction button pressed.");
+        var leaveEvent = new LeaveFactionRequestEvent();
+        // Raise the network event to send it to the server
+        _ent.RaisePredictiveEvent(leaveEvent); // Use RaisePredictiveEvent for client-initiated actions
+        _sawmill.Info("Sent LeaveFactionRequestEvent to server.");
+        if (_ent.TryGetComponent<CivFactionComponent>(_player.LocalEntity, out var factionComp))
+        {
+            factionComp.SetFaction("");
+        }
+        // Attempt to refresh the window state immediately.
+        // RefreshFactionWindowState(); // Removed: UI will update via PlayerFactionStatusChangedEvent
+    }
+
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    private void HandleInvitePlayerPressed()
+    {
+        _sawmill.Debug("Invite Player button pressed.");
+
+        if (_window == null)
+        {
+            _sawmill.Error("Attempted to invite player, but FactionWindow is null!");
+            return;
+        }
+
+        // Get the target player's name from the window's input field
+        var targetPlayerName = _window.InvitePlayerNameInputText.Trim();
+
+        if (string.IsNullOrWhiteSpace(targetPlayerName))
+        {
+            _sawmill.Debug("Invite player: Name field is empty.");
+            _popupSystem?.PopupCursor("Player name cannot be empty.", PopupType.SmallCaution);
+            return;
+        }
+
+        _sawmill.Info($"Attempting to invite player: '{targetPlayerName}'");
+
+        // Find the player session by name (case-insensitive search)
+        var targetSession = _player.Sessions.FirstOrDefault( // Sessions are ICommonSession on client
+            s => s.Name.Equals(targetPlayerName, StringComparison.OrdinalIgnoreCase) // Name is available on ICommonSession
+        );
+
+        if (targetSession == null)
+        {
+            var notFoundMsg = $"Player '{targetPlayerName}' not found.";
+            _sawmill.Warning(notFoundMsg);
+            _popupSystem?.PopupCursor(notFoundMsg, PopupType.SmallCaution);
+            return;
+        }
+
+        // Player found, get their NetUserId. UserId is available on ICommonSession.
+        NetUserId targetUserId = targetSession.UserId; // Correctly accesses UserId from ICommonSession
+
+        // Create the event
+        var inviteEvent = new InviteFactionRequestEvent(targetUserId);
+
+        // Send the event to the server
+        _ent.RaisePredictiveEvent(inviteEvent);
+        _sawmill.Info($"Sent InviteFactionRequestEvent for target player '{targetPlayerName}' (ID: {targetUserId}) to server.");
+        _popupSystem?.PopupCursor($"Invite sent to {targetPlayerName}.", PopupType.Small);
+
+        _window.ClearInvitePlayerNameInput(); // Clear the input field
+    }
+
+    /// <summary>
+    /// Refreshes the faction window's main view (in/not in faction) and the faction list.
+    /// Call this after actions that might change the player's faction status or the list of factions.
+    /// <summary>
+    /// Updates the faction window UI to reflect the player's current faction status and the latest faction list.
+    /// </summary>
+    private void RefreshFactionWindowState()
+    {
+        if (_window == null)
+        {
+            _sawmill.Warning("RefreshFactionWindowState called but _window is null!");
+            return;
+        }
+        if (!_window.IsOpen) // No need to refresh if not open
+        {
+            _sawmill.Debug("RefreshFactionWindowState called but window is not open.");
+            return;
+        }
+
+        _sawmill.Debug("Refreshing faction window state...");
+        var (isInFaction, factionName) = GetPlayerFactionStatus();
+        _window.UpdateState(isInFaction, factionName); // This updates NotInFactionView vs InFactionView
+
+        HandleListFactionsPressed(); // This updates the FactionListLabel
+
+        _sawmill.Debug("Faction window state refreshed.");
+    }
+
+    /// <summary>
+    /// Unsubscribes the faction button from its pressed event and deactivates its pressed state.
+    /// </summary>
+    public void UnloadButton()
+    {
+        if (FactionButton == null)
+        {
+            _sawmill.Debug("FactionButton is null during UnloadButton, cannot unsubscribe.");
+            return;
+        }
+        FactionButton.OnPressed -= FactionButtonPressed;
+        _sawmill.Debug("Unsubscribed from FactionButton.OnPressed.");
+        // Also deactivate button state if window is closed during unload
+        DeactivateButton();
+    }
+
+    /// <summary>
+    /// Subscribes to the faction button's press event and synchronises its pressed state with the faction window's visibility.
+    /// </summary>
+    public void LoadButton()
+    {
+        if (FactionButton == null)
+        {
+            // This might happen if the UI loads slightly out of order.
+            // Could add a small delay/retry or ensure GameTopMenuBar is ready first.
+            _sawmill.Warning("FactionButton is null during LoadButton. Button press won't work yet.");
+            return; // Can't subscribe if button doesn't exist yet
+        }
+        // Prevent double-subscribing
+        FactionButton.OnPressed -= FactionButtonPressed;
+        FactionButton.OnPressed += FactionButtonPressed;
+        _sawmill.Debug("Subscribed to FactionButton.OnPressed.");
+        // Update button state based on current window state
+        if (_window != null)
+            FactionButton.Pressed = _window.IsOpen;
+    }
+
+    /// <summary>
+    /// Sets the faction button's pressed state to inactive if the button exists.
+    /// </summary>
+    private void DeactivateButton()
+    {
+        if (FactionButton == null) return;
+        FactionButton.Pressed = false;
+        _sawmill.Debug("Deactivated FactionButton visual state.");
+    }
+
+    /// <summary>
+    /// Sets the faction button's pressed state to active if the button exists.
+    /// </summary>
+    private void ActivateButton()
+    {
+        if (FactionButton == null) return;
+        FactionButton.Pressed = true;
+        _sawmill.Debug("Activated FactionButton visual state.");
+    }
+
+    /// <summary>
+    /// Closes the faction window if it exists and is currently open.
+    /// </summary>
+    private void CloseWindow()
+    {
+        if (_window == null)
+        {
+            _sawmill.Warning("CloseWindow called but _window is null.");
+            return;
+        }
+        if (_window.IsOpen) // Only close if open
+        {
+            _window.Close();
+            _sawmill.Debug("FactionWindow closed via CloseWindow().");
+        }
+    }
+
+    /// <summary>
+    /// Handles the faction button press event by toggling the faction window's visibility.
+    /// </summary>
+    private void FactionButtonPressed(ButtonEventArgs args)
+    {
+        _sawmill.Debug("FactionButton pressed, calling ToggleWindow.");
+        ToggleWindow();
+    }
+
+    /// <summary>
+    /// Toggles the visibility of the faction management window, updating its state and synchronising the faction button's visual state.
+    /// </summary>
+    private void ToggleWindow()
+    {
+        _sawmill.Debug($"ToggleWindow called. Window is null: {_window == null}");
+        if (_window == null)
+        {
+            _sawmill.Error("Attempted to toggle FactionWindow, but it's null!");
+            // Maybe try to recreate it? Or just log the error.
+            // For now, just return to prevent NullReferenceException
+            return;
+        }
+
+        _sawmill.Debug($"Window IsOpen: {_window.IsOpen}");
+        if (_window.IsOpen)
+        {
+            CloseWindow();
+        }
+        else
+        {
+            _sawmill.Debug("Opening FactionWindow...");
+            // Get current status *before* opening
+            var (isInFaction, factionName) = GetPlayerFactionStatus();
+            _sawmill.Debug($"Player status: IsInFaction={isInFaction}, FactionName={factionName ?? "null"}");
+
+            // Update the window state (which view to show)
+            _window.UpdateState(isInFaction, factionName);
+            _sawmill.Debug("FactionWindow state updated.");
+
+            // Open the window
+            _window.Open();
+            _sawmill.Debug("FactionWindow opened.");
+
+            // Optionally, refresh the list immediately on open
+            // This ensures the faction list is populated when the window is first opened.
+            HandleListFactionsPressed();
+        }
+
+        // Update button visual state AFTER toggling
+        // Use null-conditional operator just in case FactionButton became null somehow
+        // FactionButton?.SetClickPressed(_window?.IsOpen ?? false); // SetClickPressed might not be what you want, .Pressed is usually better for toggle state
+        if (FactionButton != null)
+        {
+            FactionButton.Pressed = _window.IsOpen;
+            _sawmill.Debug($"FactionButton visual state set to Pressed: {FactionButton.Pressed}");
+        }
+    }
+}

+ 87 - 0
Content.Client/UserInterface/Systems/Faction/Windows/FactionWindow.xaml

@@ -0,0 +1,87 @@
+<windows:FactionWindow
+    xmlns="https://spacestation14.io"
+    xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
+    xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Faction.Windows"
+    Title="Factions"
+    MinWidth="400"
+    MinHeight="350">
+    <!-- Increased MinHeight slightly -->
+    <BoxContainer Orientation="Vertical"
+                  Margin="5">
+
+        <!-- View when NOT in a faction -->
+        <BoxContainer Name="NotInFactionView"
+                      Orientation="Vertical"
+                      Visible="True">
+            <Label Text="You are not currently in a faction."
+                   Align="Center"
+                   Margin="0 0 0 10"/>
+
+            <!-- *** ADDED THIS SECTION *** -->
+            <BoxContainer Orientation="Horizontal"
+                          Margin="0 5">
+                <Label Text="Faction Name:"
+                       MinWidth="100"
+                       VerticalAlignment="Center"/>
+                <LineEdit Name="FactionNameInput"
+                          Access="Public"
+                          HorizontalExpand="True"
+                          PlaceHolder="Enter desired faction name"/>
+            </BoxContainer>
+            <!-- *** END ADDED SECTION *** -->
+
+            <Button Name="CreateFactionButton"
+                    Text="Create Faction"
+                    Access="Public"
+                    Margin="0 5"/>
+            <Button Name="ListFactionsButtonNotInFaction"
+                    Text="List Factions"
+                    Access="Public"
+                    Margin="0 5"/>
+        </BoxContainer>
+
+        <!-- View when IN a faction -->
+        <BoxContainer Name="InFactionView"
+                      Orientation="Vertical"
+                      Visible="False">
+            <Label Name="CurrentFactionLabel"
+                   Text="Current Faction: -"
+                   Align="Center"
+                   Margin="0 0 0 10"/>
+            <!-- Input for inviting a player -->
+            <BoxContainer Orientation="Horizontal"
+                    Margin="0 0 0 5">
+                <Label Text="Player to Invite:"
+                       MinWidth="100"
+                       VerticalAlignment="Center"/>
+                <LineEdit Name="InvitePlayerNameInput"
+                          Access="Public"
+                          HorizontalExpand="True"
+                          PlaceHolder="Enter player name"/>
+            </BoxContainer>
+            <Button Name="InvitePlayerButton"
+                    Text="Invite Player"
+                    Access="Public"
+                    Margin="0 5"/>
+            <Button Name="LeaveFactionButton"
+                    Text="Leave Faction"
+                    Access="Public"
+                    Margin="0 5"/>
+            <Button Name="ListFactionsButtonInFaction"
+                    Text="List Factions"
+                    Access="Public"
+                    Margin="0 5"/>
+        </BoxContainer>
+
+        <!-- Area to display the faction list -->
+        <ScrollContainer VerticalExpand="True"
+                         Margin="0 10 0 0">
+            <Label Name="FactionListLabel"
+                   Text="Faction list will appear here..."
+                   Access="Public"/>
+            <!-- Consider using RichTextLabel for better formatting later -->
+            <!-- <RichTextLabel Name="FactionListLabel" /> -->
+        </ScrollContainer>
+
+    </BoxContainer>
+</windows:FactionWindow>

+ 111 - 0
Content.Client/UserInterface/Systems/Faction/Windows/FactionWindow.xaml.cs

@@ -0,0 +1,111 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls; // Required for ButtonEventArgs
+using System; // Required for Action
+// Remove the duplicate/unused using directive if FactionUIController isn't directly used here
+using Robust.Shared.Log; // Required for ISawmill
+// using Content.Client.UserInterface.Systems.Faction;
+
+namespace Content.Client.UserInterface.Systems.Faction.Windows;
+
+[GenerateTypedNameReferences]
+public sealed partial class FactionWindow : DefaultWindow
+{
+    // For logging within the window itself
+    private static readonly ISawmill Sawmill = Logger.GetSawmill("faction.window");
+    // Events for the controller to subscribe to
+    public event Action? OnListFactionsPressed;
+    public event Action? OnCreateFactionPressed;
+    public event Action? OnLeaveFactionPressed;
+    public event Action? OnInvitePlayerPressed;
+
+    // This property relies on the XAML generator succeeding
+    public string FactionNameInputText => FactionNameInput?.Text ?? string.Empty;
+    // Property for the new invite player name input
+    public string InvitePlayerNameInputText => InvitePlayerNameInput?.Text ?? string.Empty;
+
+    /// <summary>
+    /// Initialises the faction window UI and connects button events to their corresponding public event handlers.
+    /// </summary>
+    public FactionWindow()
+    {
+        RobustXamlLoader.Load(this);
+
+        // Wire up button presses to invoke the public events
+        // These lines will only compile AFTER the XAML is fixed and the project is rebuilt
+        ListFactionsButtonNotInFaction.OnPressed += (args) => OnListFactionsPressed?.Invoke();
+        ListFactionsButtonInFaction.OnPressed += (args) => OnListFactionsPressed?.Invoke(); // Both list buttons trigger the same event
+        CreateFactionButton.OnPressed += (args) => OnCreateFactionPressed?.Invoke();
+        LeaveFactionButton.OnPressed += (args) => OnLeaveFactionPressed?.Invoke();
+        InvitePlayerButton.OnPressed += (args) => OnInvitePlayerPressed?.Invoke();
+
+        // The closing brace for the constructor is here
+    } // <-- END OF CONSTRUCTOR
+
+    /// <summary>
+    /// Clears the text from the faction name input field if it exists.
+    /// </summary>
+    public void ClearFactionNameInput()
+    {
+        // This line relies on the XAML generator succeeding
+        if (FactionNameInput != null)
+            FactionNameInput.Text = string.Empty;
+    }
+
+    /// <summary>
+    /// Clears the text from the invite player name input field, if it exists.
+    /// </summary>
+    public void ClearInvitePlayerNameInput()
+    {
+        // This line relies on the XAML generator succeeding
+        if (InvitePlayerNameInput != null)
+            InvitePlayerNameInput.Text = string.Empty;
+    }
+
+    /// <summary>
+    /// Updates the window's view based on whether the player is in a faction.
+    /// </summary>
+    /// <param name="isInFaction">True if the player is in a faction, false otherwise.</param>
+    /// <summary>
+    /// Updates the UI to reflect whether the player is currently in a faction, toggling relevant views and displaying the current faction name if applicable.
+    /// </summary>
+    /// <param name="isInFaction">Indicates whether the player is in a faction.</param>
+    /// <param name="factionName">The name of the faction if <paramref name="isInFaction"/> is true; otherwise, ignored.</param>
+    public void UpdateState(bool isInFaction, string? factionName = null)
+    {
+        Sawmill.Debug($"UpdateState called. isInFaction: {isInFaction}, factionName: '{factionName ?? "null"}'");
+
+        // These lines rely on the XAML generator succeeding
+        NotInFactionView.Visible = !isInFaction;
+        InFactionView.Visible = isInFaction;
+
+        Sawmill.Debug($"Post-UpdateState: NotInFactionView.Visible: {NotInFactionView.Visible}, InFactionView.Visible: {InFactionView.Visible}");
+
+        if (isInFaction)
+        {
+            // This line relies on the XAML generator succeeding
+            CurrentFactionLabel.Text = $"Current Faction: {factionName ?? "Unknown"}";
+            Sawmill.Debug($"CurrentFactionLabel text set to: '{CurrentFactionLabel.Text}'");
+        }
+
+        // The FactionListLabel will be updated by the FactionUIController calling HandleListFactionsPressed
+        // after this UpdateState call, or when the "List Factions" button is pressed.
+        Sawmill.Debug("UpdateState finished (main view updated). Faction list will be updated separately by controller if needed.");
+    }
+
+    /// <summary>
+    /// Updates the label displaying the list of factions.
+    /// </summary>
+    /// <summary>
+    /// Updates the faction list display with the provided text data.
+    /// </summary>
+    /// <param name="listData">The text to display in the faction list label.</param>
+    public void UpdateFactionList(string listData)
+    {
+        // This line relies on the XAML generator succeeding
+        FactionListLabel.Text = listData;
+        // If using RichTextLabel:
+        // FactionListLabel.SetMarkup(listData);
+    }
+} // <-- END OF CLASS

+ 34 - 45
Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml

@@ -1,116 +1,105 @@
 <widgets:GameTopMenuBar xmlns="https://spacestation14.io"
-           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-           xmlns:style="clr-namespace:Content.Client.Stylesheets"
-           xmlns:ic="clr-namespace:Robust.Shared.Input;assembly=Robust.Shared"
-           xmlns:is="clr-namespace:Content.Shared.Input;assembly=Content.Shared"
-           xmlns:xe="clr-namespace:Content.Client.UserInterface.XamlExtensions"
-           xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
-           xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
-           Name = "MenuButtons"
-           VerticalExpand="False"
-           Orientation="Horizontal"
-           HorizontalAlignment="Stretch"
-           VerticalAlignment="Top"
-           SeparationOverride="5"
->
+                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                        xmlns:style="clr-namespace:Content.Client.Stylesheets"
+                        xmlns:ic="clr-namespace:Robust.Shared.Input;assembly=Robust.Shared"
+                        xmlns:is="clr-namespace:Content.Shared.Input;assembly=Content.Shared"
+                        xmlns:xe="clr-namespace:Content.Client.UserInterface.XamlExtensions"
+                        xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+                        xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
+                        Name="MenuButtons"
+                        VerticalExpand="False"
+                        Orientation="Horizontal"
+                        HorizontalAlignment="Stretch"
+                        VerticalAlignment="Top"
+                        SeparationOverride="5">
     <ui:MenuButton
         Name="EscapeButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/hamburger.svg.192dpi.png'}"
-        BoundKey = "{x:Static ic:EngineKeyFunctions.EscapeMenu}"
+        BoundKey="{x:Static ic:EngineKeyFunctions.EscapeMenu}"
         ToolTip="{Loc 'game-hud-open-escape-menu-button-tooltip'}"
         MinSize="70 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"/>
     <ui:MenuButton
         Name="GuidebookButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/VerbIcons/information.svg.192dpi.png'}"
         ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenGuidebook}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="CharacterButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/character.svg.192dpi.png'}"
         ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="EmotesButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
         ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="CraftingButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/hammer.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
         ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="ActionButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
         ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="FactionButton"
         Access="Internal"
-        Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenFactionsMenu}"
+        Icon="{xe:Tex '/Textures/Interface/flag.png'}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenFactionsMenu}"
         ToolTip="{Loc 'game-hud-open-factions-menu-button-tooltip'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="AdminButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/gavel.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
         ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="SandboxButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/sandbox.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
         ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"/>
     <ui:MenuButton
         Name="AHelpButton"
         Access="Internal"
         Icon="{xe:Tex '/Textures/Interface/info.svg.192dpi.png'}"
-        BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
+        BoundKey="{x:Static is:ContentKeyFunctions.OpenAHelp}"
         ToolTip="{Loc 'ui-options-function-open-a-help'}"
         MinSize="42 64"
         HorizontalExpand="True"
-        AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
-        />
+        AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"/>
 </widgets:GameTopMenuBar>

+ 9 - 5
Content.Server/Chat/Systems/ChatSystem.cs

@@ -313,7 +313,14 @@ private void OnGameChange(GameRunLevelChangedEvent ev)
     /// <param name="message">The contents of the message</param>
     /// <param name="sender">The sender (Communications Console in Communications Console Announcement)</param>
     /// <param name="playSound">Play the announcement sound</param>
-    /// <param name="colorOverride">Optional color for the announcement message</param>
+    /// <summary>
+    /// Sends a global announcement message to all players, optionally specifying the sender name and message colour.
+    /// </summary>
+    /// <param name="message">The announcement text to broadcast.</param>
+    /// <param name="sender">Optional name to display as the sender of the announcement. Defaults to a generic announcement sender if not provided.</param>
+    /// <param name="playSound">Unused parameter; sound is not played.</param>
+    /// <param name="announcementSound">Unused parameter; announcement sound is not played.</param>
+    /// <param name="colorOverride">Optional colour for the announcement message.</param>
     public void DispatchGlobalAnnouncement(
         string message,
         string? sender = null,
@@ -326,10 +333,7 @@ private void OnGameChange(GameRunLevelChangedEvent ev)
 
         var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
         _chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
-        if (playSound)
-        {
-            _audio.PlayGlobal(announcementSound == null ? DefaultAnnouncementSound : _audio.ResolveSound(announcementSound), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
-        }
+
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global announcement from {sender}: {message}");
     }
 

+ 350 - 0
Content.Server/Civ14/CivFactions/CivFactionsSystem.cs

@@ -0,0 +1,350 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Civ14.CivFactions;
+using Content.Shared.Popups; // Use Shared Popups
+using Robust.Server.Player;
+using Robust.Shared.Player; // Required for Filter, ICommonSession
+using Robust.Shared.Network; // Required for NetUserId, INetChannel
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Shared.Chat;
+using Robust.Shared.Map.Components;
+using Robust.Shared.GameObjects; // Required for EntityUid
+using Content.Server.GameTicking;
+
+namespace Content.Server.Civ14.CivFactions;
+
+public sealed class CivFactionsSystem : EntitySystem
+{
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly ChatSystem _chatSystem = default!;
+    [Dependency] private readonly IChatManager _chatManager = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly IEntityManager _entityManager = default!; // Use IEntityManager
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    private EntityUid? _factionsEntity;
+    private CivFactionsComponent? _factionsComponent;
+
+    /// <summary>
+    /// Initialises the faction system, ensuring the global factions component exists and subscribing to relevant network events for faction management.
+    /// </summary>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // Attempt to find the global factions component on startup
+        EnsureFactionsComponent();
+
+        // Subscribe to network events
+        SubscribeNetworkEvent<CreateFactionRequestEvent>(OnCreateFactionRequest);
+        SubscribeNetworkEvent<LeaveFactionRequestEvent>(OnLeaveFactionRequest);
+        SubscribeNetworkEvent<InviteFactionRequestEvent>(OnInviteFactionRequest);
+        SubscribeNetworkEvent<AcceptFactionInviteEvent>(OnAcceptFactionInvite);
+    }
+
+    /// <summary>
+    /// Performs cleanup operations when the faction system is shut down.
+    /// </summary>
+    public override void Shutdown()
+    {
+        base.Shutdown();
+    }
+
+    /// <summary>
+    /// Ensures the global CivFactionsComponent exists and caches its reference.
+    /// Creates one if necessary (e.g., attached to the first map found).
+    /// <summary>
+    /// Ensures that a global CivFactionsComponent exists and is cached, creating one on a map entity if necessary.
+    /// </summary>
+    /// <returns>True if the factions component is available and cached; false if it could not be ensured.</returns>
+    private bool EnsureFactionsComponent()
+    {
+        if (!_gameTicker.IsGameRuleActive("FactionRule"))
+        {
+            Log.Info($"Factions are disabled on this map.");
+            return false;
+        }
+        if (_factionsComponent != null && !_entityManager.Deleted(_factionsEntity))
+            return true; // Already cached and valid
+
+        var query = EntityQueryEnumerator<CivFactionsComponent>();
+        if (query.MoveNext(out var owner, out var comp))
+        {
+            _factionsEntity = owner;
+            _factionsComponent = comp;
+            Log.Info($"Found existing CivFactionsComponent on entity {_entityManager.ToPrettyString(owner)}");
+            return true;
+        }
+        else
+        {
+            var mapQuery = EntityQueryEnumerator<MapComponent>();
+            if (mapQuery.MoveNext(out var mapUid, out _))
+            {
+                Log.Info($"No CivFactionsComponent found. Creating one on map entity {_entityManager.ToPrettyString(mapUid)}.");
+                _factionsComponent = _entityManager.AddComponent<CivFactionsComponent>(mapUid);
+                _factionsEntity = mapUid;
+                return true;
+            }
+            else
+            {
+                Log.Error("Could not find CivFactionsComponent and no map entity found to attach a new one!");
+                _factionsComponent = null;
+                _factionsEntity = null;
+                return false;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Handles a request to create a new faction, validating the faction name and player status, and adds the player as the initial member if successful.
+    /// </summary>
+
+    private void OnCreateFactionRequest(CreateFactionRequestEvent msg, EntitySessionEventArgs args)
+    {
+        if (!EnsureFactionsComponent())
+        {
+            return;
+        }
+        var sourceEntity = _factionsEntity ?? EntityUid.Invalid; // Use Invalid if component entity is somehow null
+
+        if (_factionsComponent == null || _factionsEntity == null)
+        {
+            Log.Error($"Player {args.SenderSession.Name} tried to create faction, but CivFactionsComponent is missing!");
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "Cannot create faction: Server configuration error.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, args.SenderSession.Channel);
+            return;
+        }
+
+        var playerSession = args.SenderSession;
+        var playerId = playerSession.UserId.ToString();
+
+        // Validation
+        if (string.IsNullOrWhiteSpace(msg.FactionName) || msg.FactionName.Length > 32)
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "Invalid faction name.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, playerSession.Channel);
+            return;
+        }
+
+        if (IsPlayerInFaction(playerSession.UserId, out _))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "You are already in a faction.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, playerSession.Channel);
+            return;
+        }
+
+        if (_factionsComponent.FactionList.Any(f => f.FactionName.Equals(msg.FactionName, StringComparison.OrdinalIgnoreCase)))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = $"Faction name '{msg.FactionName}' is already taken.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, playerSession.Channel);
+            return;
+        }
+
+        // Create the new faction component
+        var newFaction = new FactionData // <-- Use FactionData
+        {
+            FactionName = msg.FactionName,
+            FactionMembers = new List<string> { playerId }
+        };
+
+        _factionsComponent.FactionList.Add(newFaction);
+        Dirty(_factionsEntity.Value, _factionsComponent);
+        Log.Info($"Player {playerSession.Name} created faction '{msg.FactionName}'.");
+
+        // Send confirmation message
+        var confirmationMsg = $"Faction '{msg.FactionName}' created successfully.";
+        _chatManager.ChatMessageToOne(ChatChannel.Notifications, confirmationMsg, confirmationMsg, sourceEntity, false, playerSession.Channel);
+
+        // Notify the client their status changed
+        var statusChangeEvent = new PlayerFactionStatusChangedEvent(true, newFaction.FactionName);
+        RaiseNetworkEvent(statusChangeEvent, playerSession.Channel); // Target the specific player
+    }
+
+    /// <summary>
+    /// Handles a player's request to leave their current faction, updating faction membership and notifying the player.
+    /// </summary>
+    private void OnLeaveFactionRequest(LeaveFactionRequestEvent msg, EntitySessionEventArgs args)
+    {
+        if (!EnsureFactionsComponent())
+        {
+            return;
+        }
+        var sourceEntity = _factionsEntity ?? EntityUid.Invalid;
+        if (_factionsComponent == null || _factionsEntity == null) return;
+
+        var playerSession = args.SenderSession;
+        var playerId = playerSession.UserId.ToString();
+
+        if (!TryGetPlayerFaction(playerSession.UserId, out var faction))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "You are not in a faction.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, playerSession.Channel);
+            return;
+        }
+
+        faction!.FactionMembers.Remove(playerId);
+        Log.Info($"Player {playerSession.Name} left faction '{faction.FactionName}'.");
+
+        // FIX: Correct arguments for ChatMessageToOne
+        var confirmationMsg = $"You have left faction '{faction.FactionName}'.";
+        _chatManager.ChatMessageToOne(ChatChannel.Notifications, confirmationMsg, confirmationMsg, sourceEntity, false, playerSession.Channel);
+
+        if (faction.FactionMembers.Count == 0)
+        {
+            _factionsComponent.FactionList.Remove(faction);
+            Log.Info($"Faction '{faction.FactionName}' disbanded as it became empty.");
+        }
+
+        Dirty(_factionsEntity.Value, _factionsComponent);
+
+        // Notify the client their status changed
+        var statusChangeEvent = new PlayerFactionStatusChangedEvent(false, null);
+        RaiseNetworkEvent(statusChangeEvent, playerSession.Channel); // Target the specific player
+    }
+
+    /// <summary>
+    /// Handles a request for a player to invite another player to their faction, performing validation and sending appropriate notifications and network events.
+    /// </summary>
+    private void OnInviteFactionRequest(InviteFactionRequestEvent msg, EntitySessionEventArgs args)
+    {
+        if (!EnsureFactionsComponent())
+        {
+            return;
+        }
+        var sourceEntity = _factionsEntity ?? EntityUid.Invalid;
+        if (_factionsComponent == null || _factionsEntity == null) return;
+
+        var inviterSession = args.SenderSession;
+        var inviterId = inviterSession.UserId;
+
+        if (!TryGetPlayerFaction(inviterId, out var inviterFaction))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "You must be in a faction to invite others.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, inviterSession.Channel);
+            return;
+        }
+
+        if (!_playerManager.TryGetSessionById(msg.TargetPlayerUserId, out var targetSession))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = "Could not find the player you tried to invite.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, inviterSession.Channel);
+            return;
+        }
+
+        if (IsPlayerInFaction(msg.TargetPlayerUserId, out _))
+        {
+            // FIX: Correct arguments for ChatMessageToOne (to inviter)
+            var inviterErrorMsg = $"{targetSession.Name} is already in a faction.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, inviterErrorMsg, inviterErrorMsg, sourceEntity, false, inviterSession.Channel);
+
+            // FIX: Correct arguments for ChatMessageToOne (to target)
+            var targetErrorMsg = $"{inviterSession.Name} tried to invite you to '{inviterFaction!.FactionName}', but you are already in a faction.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, targetErrorMsg, targetErrorMsg, sourceEntity, false, targetSession.Channel);
+            return;
+        }
+
+        var offerEvent = new FactionInviteOfferEvent(inviterSession.Name, inviterFaction!.FactionName, inviterId);
+        RaiseNetworkEvent(offerEvent, Filter.SinglePlayer(targetSession));
+
+        // FIX: Correct arguments for ChatMessageToOne (confirmation to inviter)
+        var inviterConfirmMsg = $"Invitation sent to {targetSession.Name}.";
+        _chatManager.ChatMessageToOne(ChatChannel.Notifications, inviterConfirmMsg, inviterConfirmMsg, sourceEntity, false, inviterSession.Channel);
+
+        // FIX: Correct arguments for ChatMessageToOne (notification to target)
+        var targetNotifyMsg = $"{inviterSession.Name} has invited you to join the faction '{inviterFaction.FactionName}'. Check your chat or notifications.";
+        _chatManager.ChatMessageToOne(ChatChannel.Notifications, targetNotifyMsg, targetNotifyMsg, sourceEntity, false, targetSession.Channel);
+
+        Log.Info($"Player {inviterSession.Name} invited {targetSession.Name} to faction '{inviterFaction.FactionName}'.");
+    }
+
+    /// <summary>
+    /// Handles a player's acceptance of a faction invitation, adding them to the specified faction and notifying them of the status change.
+    /// </summary>
+    private void OnAcceptFactionInvite(AcceptFactionInviteEvent msg, EntitySessionEventArgs args)
+    {
+        if (!EnsureFactionsComponent())
+        {
+            return;
+        }
+        var sourceEntity = _factionsEntity ?? EntityUid.Invalid;
+        if (_factionsComponent == null || _factionsEntity == null) return;
+
+        var accepterSession = args.SenderSession;
+        var accepterId = accepterSession.UserId;
+        var accepterIdStr = accepterId.ToString();
+
+        if (IsPlayerInFaction(accepterId, out var currentFaction))
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = $"You cannot accept the invite, you are already in faction '{currentFaction!.FactionName}'.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, accepterSession.Channel);
+            return;
+        }
+
+        var targetFaction = _factionsComponent.FactionList.FirstOrDefault(f => f.FactionName.Equals(msg.FactionName, StringComparison.OrdinalIgnoreCase));
+        if (targetFaction == null)
+        {
+            // FIX: Correct arguments for ChatMessageToOne
+            var errorMsg = $"The faction '{msg.FactionName}' no longer exists.";
+            _chatManager.ChatMessageToOne(ChatChannel.Notifications, errorMsg, errorMsg, sourceEntity, false, accepterSession.Channel);
+            return;
+        }
+
+        targetFaction.FactionMembers.Add(accepterIdStr);
+        Dirty(_factionsEntity.Value, _factionsComponent);
+
+        // FIX: Correct arguments for ChatMessageToOne
+        var confirmationMsg = $"You have joined faction '{targetFaction.FactionName}'.";
+        _chatManager.ChatMessageToOne(ChatChannel.Notifications, confirmationMsg, confirmationMsg, sourceEntity, false, accepterSession.Channel);
+        Log.Info($"Player {accepterSession.Name} accepted invite and joined faction '{targetFaction.FactionName}'.");
+
+        // Notify the client their status changed
+        var statusChangeEvent = new PlayerFactionStatusChangedEvent(true, targetFaction.FactionName);
+        RaiseNetworkEvent(statusChangeEvent, accepterSession.Channel); // Target the specific player
+    }
+
+
+    /// <summary>
+    /// Determines whether the specified player is a member of any faction.
+    /// </summary>
+    /// <param name="userId">The user ID of the player to check.</param>
+    /// <param name="faction">
+    /// When this method returns, contains the faction the player belongs to if found; otherwise, null.
+    /// </param>
+    /// <returns>True if the player is in a faction; otherwise, false.</returns>
+
+    public bool IsPlayerInFaction(NetUserId userId, out FactionData? faction) // <-- Use FactionData
+    {
+        faction = null;
+        if (_factionsComponent == null)
+            return false;
+
+        var playerIdStr = userId.ToString();
+        foreach (var f in _factionsComponent.FactionList)
+        {
+            if (f.FactionMembers.Contains(playerIdStr))
+            {
+                faction = f;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /// <summary>
+    /// Attempts to find the faction that the specified player belongs to.
+    /// </summary>
+    /// <param name="userId">The user ID of the player.</param>
+    /// <param name="faction">When this method returns, contains the player's faction if found; otherwise, null.</param>
+    /// <returns>True if the player is a member of a faction; otherwise, false.</returns>
+    public bool TryGetPlayerFaction(NetUserId userId, out FactionData? faction) // <-- Use FactionData
+    {
+        return IsPlayerInFaction(userId, out faction);
+    }
+}

+ 12 - 0
Content.Server/GameTicking/Rules/Components/FactionRuleComponent.cs

@@ -0,0 +1,12 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+[RegisterComponent]
+public sealed partial class FactionRuleComponent : Component
+{
+    /// <summary>
+    /// Is Nomads factions module active
+    /// </summary>
+    [DataField("active")]
+    public bool Active = true;
+
+}

+ 24 - 0
Content.Shared/Civ14/CivFactions/CivFactionComponent.cs

@@ -0,0 +1,24 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Civ14.CivFactions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class CivFactionComponent : Component
+{
+    /// <summary>
+    /// The total weight of the entity, which is calculated
+    /// by recursive passes over all children with this component
+    /// </summary>
+    [ViewVariables]
+    public string FactionName { get; set; } = "";
+
+    /// <summary>
+    /// Sets the faction name associated with this component.
+    /// </summary>
+    /// <param name="factionName">The name of the faction to assign.</param>
+    public void SetFaction(string factionName)
+    {
+        FactionName = factionName;
+
+    }
+}

+ 25 - 0
Content.Shared/Civ14/CivFactions/CivFactionsComponent.cs

@@ -0,0 +1,25 @@
+using System;
+using Content.Shared.Clothing.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Civ14.CivFactions;
+
+[AutoGenerateComponentState] // Add this attribute
+[RegisterComponent]
+[NetworkedComponent]
+public sealed partial class CivFactionsComponent : Component
+{
+    /// <summary>
+    /// The list of current factions in the game.
+    /// </summary>
+    [DataField("factionList"), AutoNetworkedField]
+    public List<FactionData> FactionList { get; set; } = new(); // <-- Use FactionData
+    /// <summary>
+    /// Check if the faction rule is enabled.
+    /// </summary>
+    [DataField("factionsActive")]
+    public bool FactionsActive { get; set; } = true;
+}

+ 134 - 0
Content.Shared/Civ14/CivFactions/CivFactionsEvents.cs

@@ -0,0 +1,134 @@
+using Robust.Shared.Serialization;
+using Robust.Shared.Network; // Required for NetUserId
+
+namespace Content.Shared.Civ14.CivFactions;
+
+/// <summary>
+/// Base class for faction-related network events for easier subscription if needed.
+/// </summary>
+[Serializable, NetSerializable]
+public abstract class BaseFactionRequestEvent : EntityEventArgs
+{
+    // Can add common fields here if necessary later
+}
+
+/// <summary>
+/// Sent from Client -> Server when a player wants to create a new faction.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class CreateFactionRequestEvent : BaseFactionRequestEvent
+{
+    public string FactionName { get; }
+
+    /// <summary>
+    /// Initialises a new request to create a faction with the specified name.
+    /// </summary>
+    /// <param name="factionName">The desired name for the new faction.</param>
+    public CreateFactionRequestEvent(string factionName)
+    {
+        FactionName = factionName;
+    }
+}
+
+/// <summary>
+/// Sent from Client -> Server when a player wants to leave their current faction.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class LeaveFactionRequestEvent : BaseFactionRequestEvent
+{
+    // No extra data needed, server knows sender.
+}
+
+/// <summary>
+/// Sent from Client -> Server when a player wants to invite another player.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class InviteFactionRequestEvent : BaseFactionRequestEvent
+{
+    /// <summary>
+    /// The NetUserId of the player being invited.
+    /// </summary>
+    public NetUserId TargetPlayerUserId { get; }
+
+    /// <summary>
+    /// Initialises a new invitation request event targeting the specified player for faction invitation.
+    /// </summary>
+    /// <param name="targetPlayerUserId">The user ID of the player to invite to the faction.</param>
+    public InviteFactionRequestEvent(NetUserId targetPlayerUserId)
+    {
+        TargetPlayerUserId = targetPlayerUserId;
+    }
+}
+
+/// <summary>
+/// Sent from Server -> Client (Target) to notify them of a faction invitation.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class FactionInviteOfferEvent : EntityEventArgs // Not inheriting BaseFactionRequestEvent
+{
+    public string InviterName { get; }
+    public string FactionName { get; }
+    public NetUserId InviterUserId { get; } /// <summary>
+    /// Initialises a new faction invitation offer with the inviter's name, faction name, and inviter's user ID.
+    /// </summary>
+    /// <param name="inviterName">The display name of the player sending the invitation.</param>
+    /// <param name="factionName">The name of the faction the invitation is for.</param>
+    /// <param name="inviterUserId">The user ID of the player sending the invitation.</param>
+
+    public FactionInviteOfferEvent(string inviterName, string factionName, NetUserId inviterUserId)
+    {
+        InviterName = inviterName;
+        FactionName = factionName;
+        InviterUserId = inviterUserId;
+    }
+}
+
+/// <summary>
+/// Sent from Client (Target) -> Server when a player accepts a faction invitation.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class AcceptFactionInviteEvent : BaseFactionRequestEvent
+{
+    /// <summary>
+    /// The name of the faction being joined.
+    /// </summary>
+    public string FactionName { get; }
+    /// <summary>
+    /// The NetUserId of the player who originally sent the invite.
+    /// </summary>
+    public NetUserId InviterUserId { get; } /// <summary>
+    /// Initialises a new event indicating that a player has accepted a faction invitation.
+    /// </summary>
+    /// <param name="factionName">The name of the faction being joined.</param>
+    /// <param name="inviterUserId">The user ID of the player who sent the invitation.</param>
+
+    public AcceptFactionInviteEvent(string factionName, NetUserId inviterUserId)
+    {
+        FactionName = factionName;
+        InviterUserId = inviterUserId;
+    }
+}
+
+// Optional: Decline event if explicit decline handling is needed beyond timeout/ignoring.
+// [Serializable, NetSerializable]
+// public sealed class DeclineFactionInviteEvent : BaseFactionRequestEvent { ... }
+
+/// <summary>
+/// Sent from Server -> Client (specific player) when their faction membership status changes.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class PlayerFactionStatusChangedEvent : EntityEventArgs
+{
+    public bool IsInFaction { get; }
+    public string? FactionName { get; } /// <summary>
+    /// Initialises a new event indicating a player's faction membership status and, if applicable, the faction's name.
+    /// </summary>
+    /// <param name="isInFaction">Whether the player is currently in a faction.</param>
+    /// <param name="factionName">The name of the faction if the player is a member; otherwise, null.</param>
+
+    public PlayerFactionStatusChangedEvent(bool isInFaction, string? factionName)
+    {
+        IsInFaction = isInFaction;
+        FactionName = factionName;
+    }
+}

+ 0 - 0
Content.Shared/Civ14/CivFactions/CivFactionsSystem.cs


+ 46 - 0
Content.Shared/Civ14/CivFactions/FactionData.cs

@@ -0,0 +1,46 @@
+using System;
+using Content.Shared.Clothing.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using System.Collections.Generic; // Required for List
+
+namespace Content.Shared.Civ14.CivFactions;
+
+// Changed from Component to a DataDefinition for use in lists.
+[DataDefinition, Serializable, NetSerializable]
+public sealed partial class FactionData
+{
+    /// <summary>
+    /// The name of the faction.
+    /// </summary>
+    [DataField("factionName")]
+    public string FactionName { get; set; } = "Unnamed Faction";
+    /// <summary>
+    /// The list of members, using the ckeys.
+    /// </summary>
+    [DataField("factionMembers")]
+    public List<string> FactionMembers { get; set; } = new List<string>();
+    /// <summary>
+    /// The current research level of the faction.
+    /// </summary>
+    [DataField("factionResearch")]
+    public float FactionResearch { get; set; } = 0f;
+    /// <summary>
+    /// The score of the faction.
+    /// </summary>
+    [DataField("factionPoints")]
+    public int FactionPoints { get; set; } = 0;
+    /// <summary>
+    /// The ammount of money in the faction's treasury.
+    /// </summary>
+    [DataField("factionTreasury")]
+    public float FactionTreasury { get; set; } = 0f;
+    /// <summary>
+    /// People registered as leaders of the faction (can invite others)
+    /// </summary>
+    [DataField("factionLeaders")]
+    public List<string> FactionLeaders { get; set; } = new List<string>();
+}
+

+ 50 - 0
Content.Shared/Civ14/CivFactions/FactionExamineSystem.cs

@@ -0,0 +1,50 @@
+using Content.Shared.Examine;
+
+namespace Content.Shared.Civ14.CivFactions;
+
+public sealed class FactionExamineSystem : EntitySystem
+{
+    /// <summary>
+    /// Subscribes to examination events for entities with a faction component to provide custom examine text based on faction membership.
+    /// </summary>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<CivFactionComponent, ExaminedEvent>(OnFactionExamine);
+    }
+
+    /// <summary>
+    /// Adds a faction membership message to the examine event, indicating whether the examined entity shares a faction with the examiner or not.
+    /// </summary>
+    /// <param name="uid">The unique identifier of the examined entity.</param>
+    /// <param name="component">The faction component of the examined entity.</param>
+    /// <param name="args">The examination event arguments.</param>
+    private void OnFactionExamine(EntityUid uid, CivFactionComponent component, ExaminedEvent args)
+    {
+
+        if (TryComp<CivFactionComponent>(args.Examiner, out var examinerFaction))
+        {
+            if (component.FactionName == "")
+            {
+                return;
+            }
+            if (component.FactionName == examinerFaction.FactionName)
+            {
+                var str = $"He is a member of your faction, [color=#007f00]{component.FactionName}[/color].";
+                args.PushMarkup(str);
+            }
+            else
+            {
+                var str = $"He is a member of [color=#7f0000]{component.FactionName}[/color].";
+                args.PushMarkup(str);
+            }
+        }
+        else
+        {
+            var str = $"He is a member of [color=#7f0000]{component.FactionName}[/color].";
+            args.PushMarkup(str);
+        }
+    }
+
+}

+ 51 - 0
Content.Shared/Civ14/CivResearch/CivResearchComponent.cs

@@ -0,0 +1,51 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.Civ14.CivResearch;
+
+[RegisterComponent]
+[NetworkedComponent]
+public sealed partial class CivResearchComponent : Component
+{
+    /// <summary>
+    /// Defines if research is currently active and progressing
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("researchEnabled")]
+    public bool ResearchEnabled { get; set; } = true;
+    /// <summary>
+    /// The current research level. From 0 to 800.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("researchLevel")]
+    public float ResearchLevel { get; set; } = 0f;
+    /// <summary>
+    /// For autoresearch, how much research increases per tick.
+    /// This defaults to 100 levels per day.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("researchSpeed")]
+    public float ResearchSpeed { get; set; } = 0.000057f;
+    /// <summary>
+    /// The maximum research level.
+    /// Should probably stay below 900 as 9 is used as the research level for disabled and futuristic stuff.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("maxResearch")]
+    public float MaxResearch { get; set; } = 800;
+    /// <summary>
+    /// Calculates the current age based on the ResearchLevel.
+    /// Age is determined by flooring the division of ResearchLevel by 100.
+    /// </summary>
+    /// <returns>The current age as an integer.</returns>
+    public int GetCurrentAge()
+    {
+        return (int)Math.Floor(ResearchLevel / 100f);
+    }
+
+}
+

+ 69 - 0
Content.Shared/Civ14/CivResearch/CivResearchSystem.cs

@@ -0,0 +1,69 @@
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Civ14.CivResearch;
+
+public sealed partial class CivResearchSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly ILogManager _logManager = default!;
+    private ISawmill _sawmill = default!;
+    /// <summary>
+    /// Sets up the research system by subscribing to map creation events and initialising the research logger.
+    /// </summary>
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
+        _sawmill = _logManager.GetSawmill("research");
+    }
+
+
+    /// <summary>
+    /// Ensures that a CivResearchComponent is attached to the entity representing a newly created map.
+    /// </summary>
+    /// <param name="ev">The event containing information about the newly created map.</param>
+    private void OnMapCreated(MapCreatedEvent ev)
+    {
+        var mapUid = _mapManager.GetMapEntityId(ev.MapId);
+        EnsureComp<CivResearchComponent>(mapUid);
+        _sawmill.Info("research", $"Ensured ResearchComponent on new map {ev.MapId} (Entity: {mapUid})");
+    }
+
+    /// <summary>
+    /// Advances research progression on all active maps by incrementing their research level if research is enabled and not yet at the maximum.
+    /// </summary>
+    /// <param name="frameTime">Elapsed time since the last update, in seconds.</param>
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        // Iterate through all active maps
+        foreach (var mapId in _mapManager.GetAllMapIds())
+        {
+            // Get the entity UID associated with the map
+            var mapUid = _mapManager.GetMapEntityId(mapId);
+
+            // Try to get the ResearchComponent from the map's entity UID
+            if (TryComp<CivResearchComponent>(mapUid, out var comp))
+            {
+                // Now run your logic
+                if (!comp.ResearchEnabled)
+                    continue;
+
+                // Use frameTime for frame-rate independent accumulation
+                // comp.ResearchLevel += comp.ResearchSpeed * frameTime * Timing.TickRate; // More robust way
+                // Or keep the original logic if ResearchSpeed is per-tick
+                if (comp.ResearchLevel >= comp.MaxResearch)
+                {
+                    continue;
+                }
+                comp.ResearchLevel += comp.ResearchSpeed;
+
+                // Mark component dirty if necessary (often handled automatically for networked components)
+                Dirty(mapUid, comp);
+            }
+        }
+    }
+}

+ 36 - 11
Content.Shared/Examine/ExamineSystemShared.cs

@@ -395,8 +395,13 @@ int Comparison(ExamineMessagePart a, ExamineMessagePart b)
         ///     a consistent order with regards to each other. This is done so that client & server will always
         ///     sort messages the same as well as grouped together properly, even if subscriptions are different.
         ///     You should wrap it in a using() block so popping automatically occurs.
+        /// <summary>
+        /// Begins a new message group for examine text, allowing related messages to be grouped and ordered together.
         /// </summary>
-        public ExamineGroupDisposable PushGroup(string groupName, int priority=0)
+        /// <param name="groupName">The name of the group for identification and ordering.</param>
+        /// <param name="priority">The priority of the group, affecting its order in the final message.</param>
+        /// <returns>A disposable object that ends the group when disposed.</returns>
+        public ExamineGroupDisposable PushGroup(string groupName, int priority = 0)
         {
             // Ensure that other examine events correctly ended their groups.
             DebugTools.Assert(_currentGroupPart == null);
@@ -425,8 +430,12 @@ private void PopGroup()
         /// then by ordinal comparison.
         /// </summary>
         /// <seealso cref="PushMarkup"/>
-        /// <seealso cref="PushText"/>
-        public void PushMessage(FormattedMessage message, int priority=0)
+        /// <summary>
+        /// Adds a formatted message as a new line to the examine message, either within the current group or as a separate part.
+        /// </summary>
+        /// <param name="message">The formatted message to add.</param>
+        /// <param name="priority">The priority for ordering this message part.</param>
+        public void PushMessage(FormattedMessage message, int priority = 0)
         {
             if (message.Nodes.Count == 0)
                 return;
@@ -448,8 +457,12 @@ public void PushMessage(FormattedMessage message, int priority=0)
         /// then by ordinal comparison.
         /// </summary>
         /// <seealso cref="PushText"/>
-        /// <seealso cref="PushMessage"/>
-        public void PushMarkup(string markup, int priority=0)
+        /// <summary>
+        /// Parses markup text and adds it as a new message part on its own line, with optional priority for ordering.
+        /// </summary>
+        /// <param name="markup">The markup-formatted string to add.</param>
+        /// <param name="priority">Optional priority for message ordering; higher values appear later.</param>
+        public void PushMarkup(string markup, int priority = 0)
         {
             PushMessage(FormattedMessage.FromMarkupOrThrow(markup), priority);
         }
@@ -460,8 +473,12 @@ public void PushMarkup(string markup, int priority=0)
         /// then by ordinal comparison.
         /// </summary>
         /// <seealso cref="PushMarkup"/>
-        /// <seealso cref="PushMessage"/>
-        public void PushText(string text, int priority=0)
+        /// <summary>
+        /// Adds a line of plain text to the examine message, optionally specifying its priority.
+        /// </summary>
+        /// <param name="text">The text to add as a separate message line.</param>
+        /// <param name="priority">The priority for ordering this message part. Higher values appear later.</param>
+        public void PushText(string text, int priority = 0)
         {
             var msg = new FormattedMessage();
             msg.AddText(text);
@@ -496,8 +513,12 @@ public void AddMessage(FormattedMessage message, int priority = 0)
         /// then by ordinal comparison.
         /// </summary>
         /// <seealso cref="AddText"/>
-        /// <seealso cref="AddMessage"/>
-        public void AddMarkup(string markup, int priority=0)
+        /// <summary>
+        /// Adds markup-formatted text inline to the examine message without a newline.
+        /// </summary>
+        /// <param name="markup">The markup-formatted string to add.</param>
+        /// <param name="priority">The priority for ordering this message part.</param>
+        public void AddMarkup(string markup, int priority = 0)
         {
             AddMessage(FormattedMessage.FromMarkupOrThrow(markup), priority);
         }
@@ -508,8 +529,12 @@ public void AddMarkup(string markup, int priority=0)
         /// then by ordinal comparison.
         /// </summary>
         /// <seealso cref="AddMarkup"/>
-        /// <seealso cref="AddMessage"/>
-        public void AddText(string text, int priority=0)
+        /// <summary>
+        /// Adds plain text inline to the examine message at the specified priority.
+        /// </summary>
+        /// <param name="text">The text to add to the message.</param>
+        /// <param name="priority">The priority for ordering this message part. Higher values appear later.</param>
+        public void AddText(string text, int priority = 0)
         {
             var msg = new FormattedMessage();
             msg.AddText(text);

+ 3 - 5
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs

@@ -126,20 +126,18 @@ private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, Compone
         LoadProfile(uid, startingSet.Profile, humanoid);
     }
 
+    /// <summary>
+    /// Adds a description to the examination text for a humanoid entity, including its identity, species, and age category.
+    /// </summary>
     private void OnExamined(EntityUid uid, HumanoidAppearanceComponent component, ExaminedEvent args)
     {
         var identity = Identity.Entity(uid, EntityManager);
         var species = GetSpeciesRepresentation(component.Species).ToLower();
         var age = GetAgeRepresentation(component.Species, component.Age);
 
-        // WWDP EDIT
         string locale = "humanoid-appearance-component-examine";
 
-        if (args.Examiner == args.Examined) // Use the selfaware locale when examining yourself
-            locale += "-selfaware";
-
         args.PushText(Loc.GetString(locale, ("user", identity), ("age", age), ("species", species)), 100); // priority for examine
-        // WWDP EDIT END
     }
 
     /// <summary>

+ 12 - 4
Content.Shared/_Stalker/Weight/SharedWeightExamineInfoSystem.cs

@@ -1,4 +1,4 @@
-using Content.Shared.Examine;
+using Content.Shared.Examine;
 
 namespace Content.Shared._Stalker.Weight;
 
@@ -11,6 +11,9 @@ public override void Initialize()
         SubscribeLocalEvent<STWeightComponent, ExaminedEvent>(OnWeightExamine);
     }
 
+    /// <summary>
+    /// Adds a colour-coded weight description to the examination event based on the entity's total weight.
+    /// </summary>
     private void OnWeightExamine(EntityUid uid, STWeightComponent component, ExaminedEvent args)
     {
         if (!args.IsInDetailsRange)
@@ -21,8 +24,8 @@ private void OnWeightExamine(EntityUid uid, STWeightComponent component, Examine
 
         if (component.Total < 50f)
         {
-             r = HexFromId(255 / 50 * (int)component.Total);
-             g = HexFromId(255);
+            r = HexFromId(255 / 50 * (int)component.Total);
+            g = HexFromId(255);
         }
 
         var colorString = $"#{r}{g}00";
@@ -31,6 +34,11 @@ private void OnWeightExamine(EntityUid uid, STWeightComponent component, Examine
         args.PushMarkup(str);
     }
 
+    /// <summary>
+    /// Converts an integer to a two-character hexadecimal string, clamping values below 0 to "00" and above 255 to "FF".
+    /// </summary>
+    /// <param name="id">The integer value to convert.</param>
+    /// <returns>A two-character hexadecimal string representing the clamped value.</returns>
     private string HexFromId(int id)
     {
         switch (id)
@@ -39,7 +47,7 @@ private string HexFromId(int id)
                 return "00";
 
             case < 16:
-                return  "0" + id.ToString("X");
+                return "0" + id.ToString("X");
 
             case > 255:
                 id = 255;

+ 1 - 0
Resources/Changelog/Changelog.yml

@@ -158,3 +158,4 @@ Entries:
     id: 14
     time: "2025-04-26T00:00:00.0000000+00:00"
     url: https://github.com/Civ13/Civ14/pull/156
+

+ 3 - 3
Resources/ConfigPresets/Build/development.toml

@@ -1,12 +1,12 @@
 [game]
 # Straight in-game baby
-lobbyenabled = true
+lobbyenabled = false
 # Dev map for faster loading & convenience
-map = "Camp"
+map = "Nomads"
 role_timers = false
 lobbyduration = 15
 disallowlatejoins = false
-defaultpreset = "tdm"
+defaultpreset = "nomads"
 
 [events]
 enabled = false

文件差异内容过多而无法显示
+ 2 - 1
Resources/Maps/civ/nomads_classic.yml


+ 3 - 3
Resources/Prototypes/Civ14/Entities/Objects/Guns/entities_bullets.yml

@@ -76,7 +76,7 @@
     - type: Projectile
       damage:
         types:
-          Piercing: 42
+          Piercing: 35
 - type: entity
   id: civ13_bullet_musketball
   name: civ13_bullet_musketball
@@ -87,7 +87,7 @@
     - type: Projectile
       damage:
         types:
-          Piercing: 49
+          Piercing: 37
 - type: entity
   id: civ13_bullet_musketball_pistol
   name: civ13_bullet_musketball_pistol
@@ -98,7 +98,7 @@
     - type: Projectile
       damage:
         types:
-          Piercing: 31
+          Piercing: 20
 - type: entity
   id: civ13_bullet_blunderbuss
   name: civ13_bullet_blunderbuss

+ 1 - 0
Resources/Prototypes/Entities/Mobs/Species/base.yml

@@ -181,6 +181,7 @@
     - type: NpcFactionMember
       factions:
         - Nomads
+    - type: CivFaction
     - type: CreamPied
     - type: Stripping
     - type: UserInterface

+ 6 - 0
Resources/Prototypes/GameRules/roundstart.yml

@@ -76,3 +76,9 @@
   parent: BaseGameRule
   components:
     - type: TeamDeathMatchRule
+- type: entity
+  parent: BaseGameRule
+  id: FactionRule
+  components:
+    - type: FactionRule
+

+ 1 - 1
Resources/Prototypes/game_presets.yml

@@ -8,6 +8,7 @@
   description: extended-description
   rules:
     - RespawnDeadRule
+    - FactionRule
 
 - type: gamePreset
   id: TDM
@@ -21,4 +22,3 @@
     - GracewallRule
     - CaptureAreaRule
     - TeamDeathMatchRule
-

+ 1 - 1
Resources/ServerInfo/Guidebook/Nomads/nomadsguide.xml

@@ -17,7 +17,7 @@
 
     -   Click the rock repeatedly with the flint to sharpen it (you will get a message when it is sharpened).
     -   Now go to a tree and right click on it. There should be an option to remove a branch from the tree, if there are any suitable ones available. This will pull a branch from the tree and place it in your hands.
-    -   Right-click on the branch and remove the leaves and sharpen it. Then, click the sharpened stick with the flint again to craft a flint axe, which serves as both a tool and a weapon.
+    -   Right-click on the branch and remove the leaves and sharpen it. Then, open the crafting menu (C) and find the **Flint Axe**. Craft it and you're ready!
 
 <GuideEntityEmbed Entity="FlintAxe" />
 

+ 1 - 0
Resources/Textures/Civ14/Weapons/Guns/mosin.rsi/meta.json

@@ -1,4 +1,5 @@
 {
+
     "version": 1,
     "license": "AGPL-3.0",
     "copyright": "Exported from https://github.com/civ13/civ13",

二进制
Resources/Textures/Interface/flag.png


+ 1 - 1
Wiki/src/guides/starter_guide.md

@@ -26,7 +26,7 @@ You can also open this guide in-game. Press **0** on the numpad or click the ico
 
     -   Click the rock repeatedly with the flint to sharpen it (you will get a message when it is sharpened).
     -   Now go to a tree and right click on it. There should be an option to remove a branch from the tree, if there are any suitable ones available. This will pull a branch from the tree and place it in your hands.
-    -   Right-click on the branch and remove the leaves and sharpen it. Then, click the sharpened stick with the flint again to craft a flint axe, which serves as both a tool and a weapon.
+    -   Right-click on the branch and remove the leaves and sharpen it. Then, open the crafting menu (C) and find the **Flint Axe**. Craft it and you're ready!
 
 ![flint axe crafting menu](./../images/flint_axe.png)
 

+ 7 - 2
mapGeneration.py

@@ -370,7 +370,11 @@ def generate_atmosphere_tiles(width, height, chunk_size):
 
 
 def generate_main_entities(tile_map, chunk_size=16, decals_by_id=None):
-    """Generates entities, decals and atmos."""
+    """
+    Generates the main map and grid entities, including tile chunks, decals, and atmospheric data.
+    
+    Divides the tile map into chunks, encodes each chunk for storage, and constructs the main map entity (UID 1) and grid entity (UID 2) with relevant components such as lighting, physics, weather, decals, and atmosphere. Decals are grouped by ID and indexed, and atmospheric data is generated per chunk. Returns a dictionary containing the main entities and their components.
+    """
     if decals_by_id is None:
         decals_by_id = {}
 
@@ -427,6 +431,7 @@ def generate_main_entities(tile_map, chunk_size=16, decals_by_id=None):
                     {"type": "MovedGrids"},
                     {"type": "Broadphase"},
                     {"type": "OccluderTree"},
+                    {"type": "CivFactions"},
                 ],
             },
             {
@@ -475,7 +480,7 @@ def generate_main_entities(tile_map, chunk_size=16, decals_by_id=None):
                         "minSeasonMinutes": 30,
                         "maxSeasonMinutes": 45,
                         "minPrecipitationDurationMinutes": 5,
-                        "maxPrecipitationDurationMinutes": 10
+                        "maxPrecipitationDurationMinutes": 10,
                     },
                     {
                         "type": "DecalGrid",

部分文件因为文件数量过多而无法显示