diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 8f7a1f39b8cb4..608d2e40eef6d 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,10 +18,17 @@
- [ ] I have added media to this PR or it does not require an in-game showcase.
+## Licensing
+
+- [ ] I give permission for any changes to the repository made in this PR to be relicensed under MIT.
+
+
**Changelog**
+Changelog must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog.
+The name that appears on the changelog will be your GitHub usernabe by default. If you wish for a different name to appear, format the symbol like so:
+:cl: My Name -->
+
+
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml
index 568281089f047..21c4b64ce8fa0 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -1,8 +1,7 @@
+ MinSize="800 450">
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml b/Content.Client/Options/UI/Tabs/AudioTab.xaml
index 2929d5b260900..f5d57dbeb69ab 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml
@@ -2,32 +2,49 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
index 1f7df3a7992eb..e35412e8991a5 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
@@ -8,6 +8,7 @@
using Robust.Shared;
using Robust.Shared.Configuration;
using Content.Shared._EE.CCVar; // EE
+using Content.Shared._VDS.CCVars; // VDS
namespace Content.Client.Options.UI.Tabs;
@@ -76,6 +77,17 @@ public AudioTab()
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.BwoinkSoundEnabled, BwoinkSoundCheckBox);
+ // VDS start
+ var acousticEnable = Control.AddOptionCheckBox(VCCVars.AcousticEnable, AcousticEnableCheckBox);
+ acousticEnable.ImmediateValueChanged += UpdateAcousticButtons;
+ Control.AddOptionCheckBox(VCCVars.AcousticHighResolution, AcousticHighResolutionCheckBox);
+ Control.AddOptionSlider(
+ VCCVars.AcousticReflectionCount,
+ SliderAcousticReflectionCount,
+ _cfg.GetCVar(VCCVars.AcousticReflectionCountMinimum),
+ _cfg.GetCVar(VCCVars.AcousticReflectionCountMaximum));
+ // VDS end
+
Control.Initialize();
}
@@ -84,6 +96,7 @@ protected override void EnteredTree()
base.EnteredTree();
_admin.AdminStatusUpdated += UpdateAdminButtonsVisibility;
UpdateAdminButtonsVisibility();
+ UpdateAcousticButtons(_cfg.GetCVar(VCCVars.AcousticEnable)); // VDS
}
protected override void ExitedTree()
@@ -98,6 +111,12 @@ private void UpdateAdminButtonsVisibility()
BwoinkSoundCheckBox.Visible = _admin.IsActive();
}
+ private void UpdateAcousticButtons(bool value) // VDS
+ {
+ AcousticHighResolutionCheckBox.Visible = value is true;
+ SliderAcousticReflectionCount.Visible = value is true;
+ }
+
private void OnMasterVolumeSliderChanged(float value)
{
// TODO: I was thinking of giving OptionsTabControlRow a flag to "set CVar immediately", but I'm deferring that
diff --git a/Content.Client/Options/UI/Tabs/MiscTab.xaml b/Content.Client/Options/UI/Tabs/MiscTab.xaml
index c1733e209dbe7..92e426140e1c8 100644
--- a/Content.Client/Options/UI/Tabs/MiscTab.xaml
+++ b/Content.Client/Options/UI/Tabs/MiscTab.xaml
@@ -1,4 +1,4 @@
-
diff --git a/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs b/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
index 79000af58c6d5..5217f1cb1b056 100644
--- a/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Content.Client.UserInterface.Screens;
using Content.Shared.CCVar;
using Content.Shared.HUD;
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
index 44b1ff95e7fea..5e51d9a88c3ec 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
@@ -7,6 +7,14 @@
+
+
+
+
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
index b850aab97643e..dec93d09b299e 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
@@ -86,13 +86,16 @@ private void GenerateButton(ListData data, ListContainerButton button)
///
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
+ /// IMP: Also hides the store button if there is no store
///
- public void Populate(List inventory, bool enabled)
+ public void Populate(List inventory, bool enabled, bool hasStore = false) // imp add hasStore
{
_enabled = enabled;
_listItems.Clear();
_amounts.Clear();
+ StoreButton.Visible = hasStore; // imp add
+
if (inventory.Count == 0 && VendingContents.Visible)
{
SearchBar.Visible = false;
diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
index 874808158d833..f36b723987d3d 100644
--- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
+++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
@@ -4,6 +4,7 @@
using Robust.Client.UserInterface;
using Robust.Shared.Input;
using System.Linq;
+using Content.Shared.Store.Components; // IMP ADD
namespace Content.Client.VendingMachines
{
@@ -26,6 +27,11 @@ protected override void Open()
_menu = this.CreateWindowCenteredLeft();
_menu.Title = EntMan.GetComponent(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
+ // IMP ADD START
+ _menu.StoreButton.OnPressed += _ =>
+ {
+ SendMessage(new VendingStoreOpenMessage());
+ };
Refresh();
}
@@ -36,7 +42,9 @@ public void Refresh()
var system = EntMan.System();
_cachedInventory = system.GetAllInventory(Owner);
- _menu?.Populate(_cachedInventory, enabled);
+ var hasStore = EntMan.HasComponent(Owner); // imp add
+
+ _menu?.Populate(_cachedInventory, enabled, hasStore); // imp add hasStore
}
public void UpdateAmounts()
diff --git a/Content.Client/_Harmony/Conspirators/EntitySystems/ConspiratorSystem.cs b/Content.Client/_Harmony/Conspirators/EntitySystems/ConspiratorSystem.cs
new file mode 100644
index 0000000000000..175c84e8b2efe
--- /dev/null
+++ b/Content.Client/_Harmony/Conspirators/EntitySystems/ConspiratorSystem.cs
@@ -0,0 +1,34 @@
+using Content.Shared._Harmony.Conspirators.Components;
+using Content.Shared._Harmony.Conspirators.EntitySystems;
+using Content.Shared.Antag;
+using Content.Shared.StatusIcon.Components;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Harmony.Conspirators.EntitySystems;
+
+public sealed class ConspiratorSystem : SharedConspiratorSystem
+{
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnConspiratorGetIcons);
+ }
+
+ private void OnConspiratorGetIcons(Entity entity, ref GetStatusIconsEvent args)
+ {
+ if (_playerManager.LocalSession?.AttachedEntity is { } playerEntity)
+ {
+ if (!HasComp(playerEntity) &&
+ !HasComp(playerEntity))
+ return;
+ }
+
+ if (_prototypeManager.TryIndex(entity.Comp.ConspiratorIcon, out var iconPrototype))
+ args.StatusIcons.Add(iconPrototype);
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs b/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs
new file mode 100644
index 0000000000000..e38d796b7249c
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs
@@ -0,0 +1,79 @@
+using Content.Client.Eui;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Eui;
+
+namespace Content.Client._Impstation.StrangeMoods.Eui;
+
+public sealed class SharedMoodsEui : BaseEui
+{
+ private readonly SharedMoodsUi _sharedMoodsUi;
+
+ public SharedMoodsEui()
+ {
+ _sharedMoodsUi = new SharedMoodsUi();
+ _sharedMoodsUi.OnCreateShared += CreateShared;
+ _sharedMoodsUi.OnSave += SaveMoods;
+ _sharedMoodsUi.OnSharedSelected += GetSharedMood;
+ }
+
+ private void CreateShared()
+ {
+ SendMessage(new SharedMoodsInitStartMessage());
+ }
+
+ private void SaveMoods()
+ {
+ var newMoods = _sharedMoodsUi.GetMoods();
+ var targetMood = _sharedMoodsUi.GetTargetMood();
+
+ if (targetMood is not { } target)
+ return;
+
+ SendMessage(new SharedMoodsSaveMessage(target, newMoods));
+ _sharedMoodsUi.SetMoods(newMoods);
+ }
+
+ private void GetSharedMood(SharedMood mood)
+ {
+ if (mood.UniqueId == null)
+ return;
+
+ SendMessage(new SharedMoodsRequestMessage(mood.UniqueId));
+ }
+
+ public override void Opened()
+ {
+ _sharedMoodsUi.OpenCentered();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ switch (msg)
+ {
+ case SharedMoodsSendMessage sendData:
+ {
+ if (sendData.Mood is not { } mood)
+ return;
+
+ _sharedMoodsUi.SetMoods(mood.Moods);
+ break;
+ }
+ case SharedMoodsInitValidMessage initData:
+ {
+ _sharedMoodsUi.PopulateDropDown(initData.AllSharedMoods, initData.Mood);
+ break;
+ }
+ }
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not SharedMoodsEuiState s)
+ return;
+
+ _sharedMoodsUi.PopulateDropDown(s.AllSharedMoods, s.MoodId);
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs b/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs
new file mode 100644
index 0000000000000..e67f077475612
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs
@@ -0,0 +1,45 @@
+using Content.Client.Eui;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Eui;
+
+namespace Content.Client._Impstation.StrangeMoods.Eui;
+
+public sealed class SharedMoodsInitEui : BaseEui
+{
+ private readonly SharedMoodsInitUi _sharedMoodsUi;
+
+ public SharedMoodsInitEui()
+ {
+ _sharedMoodsUi = new SharedMoodsInitUi();
+ _sharedMoodsUi.OnNameAccepted += AcceptName;
+ }
+
+ private void AcceptName(string name)
+ {
+ SendMessage(new SharedMoodsInitAcceptMessage(name));
+ }
+
+ public override void Opened()
+ {
+ _sharedMoodsUi.OpenCentered();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ switch (msg)
+ {
+ case SharedMoodsInitValidMessage:
+ {
+ _sharedMoodsUi.Close();
+ break;
+ }
+ case SharedMoodsInitErrorMessage:
+ {
+ _sharedMoodsUi.ShowError();
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs b/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs
new file mode 100644
index 0000000000000..3e270134f81e8
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs
@@ -0,0 +1,73 @@
+using Content.Client.Eui;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Eui;
+
+namespace Content.Client._Impstation.StrangeMoods.Eui;
+
+public sealed class StrangeMoodsEui : BaseEui
+{
+ private readonly StrangeMoodUi _strangeMoodUi;
+ private NetEntity _target;
+
+ public StrangeMoodsEui()
+ {
+ _strangeMoodUi = new StrangeMoodUi();
+ _strangeMoodUi.OnGenerate += GenerateMood;
+ _strangeMoodUi.OnSave += SaveMoods;
+ _strangeMoodUi.OnSharedSelected += GetSharedMood;
+ }
+
+ private void GenerateMood()
+ {
+ SendMessage(new StrangeMoodsGenerateRequestMessage(_target));
+ }
+
+ private void SaveMoods()
+ {
+ var newMoods = _strangeMoodUi.GetMoods();
+ var sharedMood = _strangeMoodUi.GetSharedMood();
+
+ SendMessage(new StrangeMoodsSaveMessage(newMoods, sharedMood?.UniqueId, _target));
+ _strangeMoodUi.SetAllMoods(newMoods, sharedMood);
+ }
+
+ private void GetSharedMood(SharedMood mood)
+ {
+ SendMessage(new StrangeMoodsSharedRequestMessage(mood.UniqueId));
+ }
+
+ public override void Opened()
+ {
+ _strangeMoodUi.OpenCentered();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ switch (msg)
+ {
+ case (StrangeMoodsGenerateSendMessage generateSent):
+ {
+ _strangeMoodUi.AddNewMood(generateSent.Mood);
+ break;
+ }
+ case (StrangeMoodsSharedSendMessage sharedSent):
+ {
+ _strangeMoodUi.SetSharedMood(sharedSent.Mood);
+ break;
+ }
+ }
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not StrangeMoodsEuiState s)
+ return;
+
+ _target = s.Target;
+ _strangeMoodUi.SetAllMoods(s.Moods, s.SharedMood);
+ _strangeMoodUi.PopulateDropDown(s.AllSharedMoods, s.SharedMood);
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs b/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs
new file mode 100644
index 0000000000000..61951ff3964d5
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs
@@ -0,0 +1,37 @@
+using Content.Client.Eui;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Eui;
+
+namespace Content.Client._Impstation.StrangeMoods.Eui;
+
+public sealed class StrangeMoodsInitEui : BaseEui
+{
+ private readonly StrangeMoodInitUi _strangeMoodUi;
+ private NetEntity _target;
+
+ public StrangeMoodsInitEui()
+ {
+ _strangeMoodUi = new StrangeMoodInitUi();
+ _strangeMoodUi.OnPresetAccepted += AcceptPreset;
+ }
+
+ private void AcceptPreset(StrangeMoodDefinition def)
+ {
+ SendMessage(new StrangeMoodsInitAcceptMessage(def, _target));
+ _strangeMoodUi.Close();
+ }
+
+ public override void Opened()
+ {
+ _strangeMoodUi.OpenCentered();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not StrangeMoodsInitEuiState s)
+ return;
+
+ _target = s.Target;
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml b/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml
new file mode 100644
index 0000000000000..93784be1e3189
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml.cs b/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml.cs
new file mode 100644
index 0000000000000..32b1f5fc69277
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/MoodContainer.xaml.cs
@@ -0,0 +1,43 @@
+using Content.Shared._Impstation.StrangeMoods;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[GenerateTypedNameReferences]
+public sealed partial class MoodContainer : BoxContainer
+{
+ public event Action? OnMoveUp;
+ public event Action? OnMoveDown;
+ public event Action? OnDelete;
+
+ public MoodContainer(StrangeMood? mood = null, bool isShared = false)
+ {
+ RobustXamlLoader.Load(this);
+
+ if (mood != null)
+ {
+ StrangeMoodTitle.Text = mood.GetLocName();
+ StrangeMoodContent.TextRope = new Rope.Leaf(mood.GetLocDesc());
+ }
+
+ if (isShared)
+ {
+ StrangeMoodTitle.Editable = false;
+ StrangeMoodContent.Editable = false;
+ ControlsContainer.Visible = false;
+
+ StrangeMoodTitle.StyleIdentifier = "StrangeMoodShared";
+ StrangeMoodContent.StyleIdentifier = "StrangeMoodShared";
+ }
+
+ MoveUp.OnPressed += _ => OnMoveUp?.Invoke();
+ MoveDown.OnPressed += _ => OnMoveDown?.Invoke();
+ Delete.OnPressed += _ => OnDelete?.Invoke();
+ }
+
+ public string MoodTitle => StrangeMoodTitle.Text;
+ public string MoodText => Rope.Collapse(StrangeMoodContent.TextRope).Trim();
+}
diff --git a/Content.Client/_Impstation/Thaven/MoodDisplay.xaml b/Content.Client/_Impstation/StrangeMoods/MoodDisplay.xaml
similarity index 100%
rename from Content.Client/_Impstation/Thaven/MoodDisplay.xaml
rename to Content.Client/_Impstation/StrangeMoods/MoodDisplay.xaml
diff --git a/Content.Client/_Impstation/Thaven/MoodDisplay.xaml.cs b/Content.Client/_Impstation/StrangeMoods/MoodDisplay.xaml.cs
similarity index 76%
rename from Content.Client/_Impstation/Thaven/MoodDisplay.xaml.cs
rename to Content.Client/_Impstation/StrangeMoods/MoodDisplay.xaml.cs
index 9dca76c08958f..888f1a46d2895 100644
--- a/Content.Client/_Impstation/Thaven/MoodDisplay.xaml.cs
+++ b/Content.Client/_Impstation/StrangeMoods/MoodDisplay.xaml.cs
@@ -1,20 +1,20 @@
using Content.Client.Message;
-using Content.Shared._Impstation.Thaven;
+using Content.Shared._Impstation.StrangeMoods;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
-namespace Content.Client._Impstation.Thaven;
+namespace Content.Client._Impstation.StrangeMoods;
[GenerateTypedNameReferences]
public sealed partial class MoodDisplay : Control
{
- private string GetSharedString()
+ private static string GetSharedString()
{
return $"[italic][font size=10][color=gray]{Loc.GetString("moods-ui-shared-mood")}[/color][/font][/italic]";
}
- public MoodDisplay(ThavenMood mood, bool shared)
+ public MoodDisplay(StrangeMood mood, bool shared)
{
RobustXamlLoader.Load(this);
diff --git a/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml b/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml
new file mode 100644
index 0000000000000..3fae266bc1812
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml.cs b/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml.cs
new file mode 100644
index 0000000000000..9f8507d74d007
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/SharedMoodsInitUi.xaml.cs
@@ -0,0 +1,28 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[GenerateTypedNameReferences]
+public sealed partial class SharedMoodsInitUi : FancyWindow
+{
+ public event Action? OnNameAccepted;
+
+ public SharedMoodsInitUi()
+ {
+ RobustXamlLoader.Load(this);
+ AcceptNameButton.OnPressed += _ => AcceptName();
+ }
+
+ private void AcceptName()
+ {
+ var name = NameInput.Text;
+ OnNameAccepted?.Invoke(name);
+ }
+
+ public void ShowError()
+ {
+ ErrorLabel.Visible = true;
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml b/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml
new file mode 100644
index 0000000000000..c75edcca1f66a
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml.cs b/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml.cs
new file mode 100644
index 0000000000000..3e2aa189d65cf
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/SharedMoodsUi.xaml.cs
@@ -0,0 +1,154 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Impstation.StrangeMoods;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[GenerateTypedNameReferences]
+public sealed partial class SharedMoodsUi : FancyWindow
+{
+ public event Action? OnCreateShared;
+ public event Action? OnSave;
+ public event Action? OnSharedSelected;
+
+ private List _moods = [];
+ private List _sharedMoods = [];
+
+ public SharedMoodsUi()
+ {
+ RobustXamlLoader.Load(this);
+ NewMoodButton.OnPressed += _ => AddNewMood();
+ CreateSharedButton.OnPressed += _ => OnCreateShared?.Invoke();
+ SaveButton.OnPressed += _ => OnSave?.Invoke();
+ SharedMoodDropDown.OnItemSelected += args => SelectItem(args.Id);
+ }
+
+ public void AddNewMood(StrangeMood? mood = null)
+ {
+ var moodControl = new MoodContainer(mood);
+ var index = _moods.Count;
+
+ moodControl.OnMoveUp += () => MoveUp(index);
+ moodControl.OnMoveDown += () => MoveDown(index);
+ moodControl.OnDelete += () => Delete(index);
+
+ MoodContainer.AddChild(moodControl);
+ _moods.Add(new StrangeMood{ MoodName = "", MoodDesc = "" });
+ }
+
+ private void SelectItem(int id)
+ {
+ SharedMoodDropDown.SelectId(id);
+ OnSharedSelected?.Invoke(_sharedMoods[id]);
+ }
+
+ public void PopulateDropDown(HashSet sharedMoods, string? selectMoodId = null)
+ {
+ SharedMoodDropDown.Clear();
+ _sharedMoods.Clear();
+
+ var i = 0;
+ var moodFound = false;
+ foreach (var mood in sharedMoods)
+ {
+ SharedMoodDropDown.AddItem(mood.UniqueId ?? Loc.GetString("strange-moods-admin-ui-unknown"), i);
+ _sharedMoods.Insert(i, mood);
+
+ if (selectMoodId != null && selectMoodId == mood.UniqueId)
+ {
+ SelectItem(i);
+ moodFound = true;
+ }
+
+ i++;
+ }
+
+ if (!moodFound)
+ SelectItem(0); // auto-load a shared mood instead of opening a blank ui
+ }
+
+ public void PopulateDropDown(HashSet sharedMoods, SharedMood? selectMood = null)
+ {
+ PopulateDropDown(sharedMoods, selectMood?.UniqueId);
+ }
+
+ public List GetMoods(bool skipEmptyCheck = false)
+ {
+ var newMoods = new List();
+
+ foreach (var control in MoodContainer.Children)
+ {
+ if (control is not MoodContainer moodControl)
+ continue;
+
+ var title = moodControl.MoodTitle;
+ if (string.IsNullOrWhiteSpace(title) && !skipEmptyCheck)
+ continue;
+
+ var moodText = moodControl.MoodText;
+ if (string.IsNullOrWhiteSpace(moodText) && !skipEmptyCheck)
+ continue;
+
+ var mood = new StrangeMood()
+ {
+ MoodName = title,
+ MoodDesc = moodText,
+ };
+
+ newMoods.Add(mood);
+ }
+
+ return newMoods;
+ }
+
+ public ProtoId? GetTargetMood()
+ {
+ return _sharedMoods[SharedMoodDropDown.SelectedId].UniqueId;
+ }
+
+ private void MoveUp(int index)
+ {
+ if (index <= 0)
+ return;
+
+ _moods = GetMoods(true);
+ (_moods[index], _moods[index - 1]) = (_moods[index - 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void MoveDown(int index)
+ {
+ if (index >= _moods.Count - 1)
+ return;
+
+ _moods = GetMoods(true);
+ (_moods[index], _moods[index + 1]) = (_moods[index + 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void Delete(int index)
+ {
+ _moods = GetMoods(true);
+ _moods.RemoveAt(index);
+
+ SetMoods(_moods);
+ }
+
+ public void SetMoods(List moods)
+ {
+ _moods = moods;
+ MoodContainer.RemoveAllChildren();
+
+ for (var i = 0; i < moods.Count; i++)
+ {
+ var index = i; // Copy for the closures
+ var moodControl = new MoodContainer(moods[i]);
+ moodControl.OnMoveUp += () => MoveUp(index);
+ moodControl.OnMoveDown += () => MoveDown(index);
+ moodControl.OnDelete += () => Delete(index);
+ MoodContainer.AddChild(moodControl);
+ }
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml b/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml
new file mode 100644
index 0000000000000..27d4f23a7c8c4
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml.cs
new file mode 100644
index 0000000000000..3add3c293fea2
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodInitUi.xaml.cs
@@ -0,0 +1,44 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Impstation.StrangeMoods;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[GenerateTypedNameReferences]
+public sealed partial class StrangeMoodInitUi : FancyWindow
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
+
+ public event Action? OnPresetAccepted;
+
+ private List _definitions = [];
+
+ public StrangeMoodInitUi()
+ {
+ RobustXamlLoader.Load(this);
+ MoodPresetDropDown.OnItemSelected += args => MoodPresetDropDown.SelectId(args.Id);
+ AcceptPresetButton.OnPressed += _ => AcceptPreset();
+
+ var definitions = _proto.EnumeratePrototypes();
+ var i = 0;
+ foreach (var proto in definitions)
+ {
+ MoodPresetDropDown.AddItem(proto.Name, i);
+
+ var def = new StrangeMoodDefinition();
+ _serialization.CopyTo(proto, ref def, notNullableOverride: true);
+ _definitions.Insert(i++, def);
+ }
+ }
+
+ private void AcceptPreset()
+ {
+ var id = MoodPresetDropDown.SelectedId;
+ var def = _definitions[id];
+ OnPresetAccepted?.Invoke(def);
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodSheetlet.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodSheetlet.cs
new file mode 100644
index 0000000000000..6f9532255609e
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodSheetlet.cs
@@ -0,0 +1,24 @@
+using Content.Client.Stylesheets;
+using Content.Client.Stylesheets.Stylesheets;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using static Content.Client.Stylesheets.StylesheetHelpers;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[CommonSheetlet]
+public sealed class StrangeMoodSheetlet : Sheetlet
+{
+ public override StyleRule[] GetRules(NanotrasenStylesheet sheet, object config)
+ {
+ return
+ [
+ E()
+ .Identifier("StrangeMoodShared")
+ .FontColor(sheet.SecondaryPalette.Text),
+ E()
+ .Identifier("StrangeMoodShared")
+ .FontColor(sheet.SecondaryPalette.Text),
+ ];
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml b/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml
new file mode 100644
index 0000000000000..cbac68d925515
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml.cs
new file mode 100644
index 0000000000000..22b737f595ff2
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodUi.xaml.cs
@@ -0,0 +1,182 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Impstation.StrangeMoods;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[GenerateTypedNameReferences]
+public sealed partial class StrangeMoodUi : FancyWindow
+{
+ public event Action? OnGenerate;
+ public event Action? OnSave;
+ public event Action? OnSharedSelected;
+
+ private List _moods = [];
+ private List _sharedMoods = [];
+
+ public StrangeMoodUi()
+ {
+ RobustXamlLoader.Load(this);
+ NewMoodButton.OnPressed += _ => AddNewMood();
+ GenerateMoodButton.OnPressed += _ => OnGenerate?.Invoke();
+ SaveButton.OnPressed += _ => OnSave?.Invoke();
+ SharedMoodDropDown.OnItemSelected += args => OnItemSelected(args.Id);
+
+ SharedMoodDropDown.AddItem(Loc.GetString("strange-moods-admin-ui-shared-mood-none"), -1);
+ }
+
+ public void AddNewMood(StrangeMood? mood = null)
+ {
+ var moodControl = new MoodContainer(mood);
+ var index = _moods.Count;
+
+ moodControl.OnMoveUp += () => MoveUp(index);
+ moodControl.OnMoveDown += () => MoveDown(index);
+ moodControl.OnDelete += () => Delete(index);
+
+ MoodContainer.AddChild(moodControl);
+ _moods.Add(new StrangeMood{ MoodName = "", MoodDesc = "" });
+ }
+
+ private void OnItemSelected(int id)
+ {
+ SharedMoodDropDown.SelectId(id);
+
+ if (id == -1)
+ {
+ SetSharedMood(); // empty the shared mood
+ return;
+ }
+
+ OnSharedSelected?.Invoke(_sharedMoods[id]);
+ }
+
+ public void PopulateDropDown(HashSet sharedMoods, SharedMood? selectMood = null)
+ {
+ SharedMoodDropDown.Clear();
+ SharedMoodDropDown.AddItem(Loc.GetString("strange-moods-admin-ui-shared-mood-none"), -1);
+
+ var i = 0;
+ foreach (var mood in sharedMoods)
+ {
+ SharedMoodDropDown.AddItem(mood.UniqueId ?? Loc.GetString("strange-moods-admin-ui-unknown"), i);
+
+ if (selectMood != null && selectMood.UniqueId == mood.UniqueId)
+ SharedMoodDropDown.SelectId(i);
+
+ _sharedMoods.Insert(i++, mood);
+ }
+ }
+
+ public List GetMoods(bool skipEmptyCheck = false)
+ {
+ var newMoods = new List();
+
+ foreach (var control in MoodContainer.Children)
+ {
+ if (control is not MoodContainer moodControl)
+ continue;
+
+ var title = moodControl.MoodTitle;
+ if (string.IsNullOrWhiteSpace(title) && !skipEmptyCheck)
+ continue;
+
+ var moodText = moodControl.MoodText;
+ if (string.IsNullOrWhiteSpace(moodText) && !skipEmptyCheck)
+ continue;
+
+ var mood = new StrangeMood()
+ {
+ MoodName = title,
+ MoodDesc = moodText,
+ };
+
+ newMoods.Add(mood);
+ }
+
+ return newMoods;
+ }
+
+ public SharedMood? GetSharedMood()
+ {
+ var id = SharedMoodDropDown.SelectedId;
+ return id == -1 ? null : _sharedMoods[id];
+ }
+
+ private void MoveUp(int index)
+ {
+ if (index <= 0)
+ return;
+
+ _moods = GetMoods(true);
+ (_moods[index], _moods[index - 1]) = (_moods[index - 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void MoveDown(int index)
+ {
+ if (index >= _moods.Count - 1)
+ return;
+
+ _moods = GetMoods(true);
+ (_moods[index], _moods[index + 1]) = (_moods[index + 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void Delete(int index)
+ {
+ _moods = GetMoods(true);
+ _moods.RemoveAt(index);
+
+ SetMoods(_moods);
+ }
+
+ public void SetAllMoods(List moods, SharedMood? sharedMoods = null)
+ {
+ SetMoods(moods);
+ SetSharedMood(sharedMoods);
+ }
+
+ public void SetMoods(List moods)
+ {
+ _moods = moods;
+ MoodContainer.RemoveAllChildren();
+
+ for (var i = 0; i < moods.Count; i++)
+ {
+ var index = i; // Copy for the closures
+ var moodControl = new MoodContainer(moods[i]);
+ moodControl.OnMoveUp += () => MoveUp(index);
+ moodControl.OnMoveDown += () => MoveDown(index);
+ moodControl.OnDelete += () => Delete(index);
+ MoodContainer.AddChild(moodControl);
+ }
+ }
+
+ public void SetSharedMood(SharedMood? sharedMood = null)
+ {
+ SharedMoodContainer.RemoveAllChildren();
+
+ if (sharedMood == null)
+ {
+ SharedMoodDropDown.SelectId(-1);
+ return;
+ }
+
+ for (var i = 0; i < _sharedMoods.Count; i++)
+ {
+ if (_sharedMoods[i].UniqueId != sharedMood.UniqueId)
+ continue;
+
+ _sharedMoods[i] = sharedMood;
+ break;
+ }
+
+ foreach (var mood in sharedMood.Moods)
+ {
+ var moodControl = new MoodContainer(mood, true);
+ SharedMoodContainer.AddChild(moodControl);
+ }
+ }
+}
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodsBoundUserInterface.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsBoundUserInterface.cs
new file mode 100644
index 0000000000000..07828681ac22c
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsBoundUserInterface.cs
@@ -0,0 +1,29 @@
+using Content.Shared._Impstation.StrangeMoods;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+[UsedImplicitly]
+public sealed class StrangeMoodsBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+ [ViewVariables]
+ private StrangeMoodsMenu? _menu;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = this.CreateWindow();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not StrangeMoodsBuiState msg)
+ return;
+
+ _menu?.Update(msg);
+ }
+}
diff --git a/Content.Client/_Impstation/Thaven/ThavenMoodsMenu.xaml b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsMenu.xaml
similarity index 100%
rename from Content.Client/_Impstation/Thaven/ThavenMoodsMenu.xaml
rename to Content.Client/_Impstation/StrangeMoods/StrangeMoodsMenu.xaml
diff --git a/Content.Client/_Impstation/Thaven/ThavenMoodsMenu.xaml.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsMenu.xaml.cs
similarity index 51%
rename from Content.Client/_Impstation/Thaven/ThavenMoodsMenu.xaml.cs
rename to Content.Client/_Impstation/StrangeMoods/StrangeMoodsMenu.xaml.cs
index 27022a1becf29..4c14801409a12 100644
--- a/Content.Client/_Impstation/Thaven/ThavenMoodsMenu.xaml.cs
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsMenu.xaml.cs
@@ -1,27 +1,30 @@
using Content.Client.UserInterface.Controls;
-using Content.Shared._Impstation.Thaven.Components;
+using Content.Shared._Impstation.StrangeMoods;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
-namespace Content.Client._Impstation.Thaven;
+namespace Content.Client._Impstation.StrangeMoods;
[GenerateTypedNameReferences]
-public sealed partial class ThavenMoodsMenu : FancyWindow
+public sealed partial class StrangeMoodsMenu : FancyWindow
{
- public ThavenMoodsMenu()
+ public StrangeMoodsMenu()
{
RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
}
- public void Update(ThavenMoodsComponent comp, ThavenMoodsBuiState state)
+ public void Update(StrangeMoodsBuiState msg)
{
MoodDisplayContainer.Children.Clear();
- foreach (var mood in state.SharedMoods)
+ foreach (var mood in msg.SharedMoods)
+ {
MoodDisplayContainer.AddChild(new MoodDisplay(mood, true));
+ }
- foreach (var mood in comp.Moods)
+ foreach (var mood in msg.Moods)
+ {
MoodDisplayContainer.AddChild(new MoodDisplay(mood, false));
+ }
}
}
diff --git a/Content.Client/_Impstation/StrangeMoods/StrangeMoodsSystem.cs b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsSystem.cs
new file mode 100644
index 0000000000000..8f903956c31a6
--- /dev/null
+++ b/Content.Client/_Impstation/StrangeMoods/StrangeMoodsSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._Impstation.StrangeMoods;
+
+namespace Content.Client._Impstation.StrangeMoods;
+
+public sealed partial class StrangeMoodsSystem : SharedStrangeMoodsSystem;
diff --git a/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml b/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml
deleted file mode 100644
index a92b1d50451e0..0000000000000
--- a/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml.cs b/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml.cs
deleted file mode 100644
index 71294ddfa7434..0000000000000
--- a/Content.Client/_Impstation/Thaven/Eui/MoodContainer.xaml.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Content.Shared._Impstation.Thaven;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Utility;
-
-namespace Content.Client._Impstation.Thaven.Eui;
-
-[GenerateTypedNameReferences]
-public sealed partial class MoodContainer : BoxContainer
-{
- public event Action? OnMoveUp;
- public event Action? OnMoveDown;
- public event Action? OnDelete;
-
- public MoodContainer(ThavenMood? mood = null)
- {
- RobustXamlLoader.Load(this);
-
- if (mood != null)
- {
- ThavenMoodTitle.Text = mood.GetLocName();
- ThavenMoodContent.TextRope = new Rope.Leaf(mood.GetLocDesc());
- }
-
- MoveUp.OnPressed += _ => OnMoveUp?.Invoke();
- MoveDown.OnPressed += _ => OnMoveDown?.Invoke();
- Delete.OnPressed += _ => OnDelete?.Invoke();
- }
-
- public string MoodTitle => ThavenMoodTitle.Text;
- public string MoodText => Rope.Collapse(ThavenMoodContent.TextRope).Trim();
-}
diff --git a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml b/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml
deleted file mode 100644
index c1a834b711373..0000000000000
--- a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
- this shit does not layout properly unless I put the horizontal boxcontainer inside of a vertical one
- ????
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml.cs b/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml.cs
deleted file mode 100644
index 08167d364c4de..0000000000000
--- a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodUi.xaml.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using Content.Client.UserInterface.Controls;
-using Content.Shared._Impstation.Thaven;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Utility;
-
-namespace Content.Client._Impstation.Thaven.Eui;
-
-[GenerateTypedNameReferences]
-public sealed partial class ThavenMoodUi : FancyWindow
-{
- public event Action? OnSave;
-
- private List _moods = new();
- private bool _shouldFollowShared = false;
-
- public ThavenMoodUi()
- {
- RobustXamlLoader.Load(this);
- NewMoodButton.OnPressed += _ => AddNewMood();
- SaveButton.OnPressed += _ => OnSave?.Invoke();
-
- ToggleSharedMoodButton.OnToggled += _ =>
- {
- _shouldFollowShared = !_shouldFollowShared;
- };
- }
-
- private void AddNewMood()
- {
- MoodContainer.AddChild(new MoodContainer());
- }
-
- public List GetMoods()
- {
- var newMoods = new List();
-
- foreach (var control in MoodContainer.Children)
- {
- if (control is not MoodContainer moodControl)
- continue;
-
- var title = moodControl.MoodTitle;
- if (string.IsNullOrWhiteSpace(title))
- continue;
-
- var moodText = moodControl.MoodText;
- if (string.IsNullOrWhiteSpace(moodText))
- continue;
-
- var mood = new ThavenMood()
- {
- MoodName = title,
- MoodDesc = moodText,
- };
-
- newMoods.Add(mood);
- }
-
- return newMoods;
- }
-
- public bool ShouldFollowShared()
- {
- return _shouldFollowShared;
- }
-
- public void SetFollowShared(bool value)
- {
- _shouldFollowShared = value;
- ToggleSharedMoodButton.Pressed = value;
- }
-
- private void MoveUp(int index)
- {
- if (index <= 0)
- return;
-
- (_moods[index], _moods[index - 1]) = (_moods[index - 1], _moods[index]);
- SetMoods(_moods);
- }
-
- private void MoveDown(int index)
- {
- if (index >= _moods.Count - 1)
- return;
-
- (_moods[index], _moods[index + 1]) = (_moods[index + 1], _moods[index]);
- SetMoods(_moods);
- }
-
- private void Delete(int index)
- {
- _moods.RemoveAt(index);
-
- SetMoods(_moods);
- }
-
- public void SetMoods(List moods)
- {
- _moods = moods;
- MoodContainer.RemoveAllChildren();
- for (var i = 0; i < moods.Count; i++)
- {
- var index = i; // Copy for the closures
- var moodControl = new MoodContainer(moods[i]);
- moodControl.OnMoveUp += () => MoveUp(index);
- moodControl.OnMoveDown += () => MoveDown(index);
- moodControl.OnDelete += () => Delete(index);
- MoodContainer.AddChild(moodControl);
- }
- }
-}
diff --git a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodsEui.cs b/Content.Client/_Impstation/Thaven/Eui/ThavenMoodsEui.cs
deleted file mode 100644
index 677a68bc0107d..0000000000000
--- a/Content.Client/_Impstation/Thaven/Eui/ThavenMoodsEui.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Content.Client.Eui;
-using Content.Shared.Eui;
-using Content.Shared._Impstation.Thaven;
-
-namespace Content.Client._Impstation.Thaven.Eui;
-
-public sealed class ThavenMoodsEui : BaseEui
-{
- private ThavenMoodUi _thavenMoodUi;
- private NetEntity _target;
-
- public ThavenMoodsEui()
- {
- _thavenMoodUi = new ThavenMoodUi();
- _thavenMoodUi.OnSave += SaveMoods;
- }
-
- private void SaveMoods()
- {
- var newMoods = _thavenMoodUi.GetMoods();
- var toggle = _thavenMoodUi.ShouldFollowShared();
- SendMessage(new ThavenMoodsSaveMessage(newMoods, toggle, _target));
- _thavenMoodUi.SetMoods(newMoods);
- }
-
- public override void Opened()
- {
- _thavenMoodUi.OpenCentered();
- }
-
- public override void HandleState(EuiStateBase state)
- {
- if (state is not ThavenMoodsEuiState s)
- return;
-
- _target = s.Target;
- _thavenMoodUi.SetFollowShared(s.FollowsShared);
- _thavenMoodUi.SetMoods(s.Moods);
- }
-}
diff --git a/Content.Client/_Impstation/Thaven/ThavenMoodSystem.cs b/Content.Client/_Impstation/Thaven/ThavenMoodSystem.cs
deleted file mode 100644
index c318c2c10070c..0000000000000
--- a/Content.Client/_Impstation/Thaven/ThavenMoodSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared._Impstation.Thaven;
-
-namespace Content.Client._Impstation.Thaven;
-
-public sealed partial class ThavenMoodSystem : SharedThavenMoodSystem;
diff --git a/Content.Client/_Impstation/Thaven/ThavenMoodsBoundUserInterface.cs b/Content.Client/_Impstation/Thaven/ThavenMoodsBoundUserInterface.cs
deleted file mode 100644
index 0f17ba21f584b..0000000000000
--- a/Content.Client/_Impstation/Thaven/ThavenMoodsBoundUserInterface.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Content.Shared._Impstation.Thaven;
-using Content.Shared._Impstation.Thaven.Components;
-using JetBrains.Annotations;
-using Robust.Client.UserInterface;
-
-namespace Content.Client._Impstation.Thaven;
-
-[UsedImplicitly]
-public sealed class ThavenMoodsBoundUserInterface : BoundUserInterface
-{
- [Dependency] private readonly IEntityManager _entMan = default!;
-
- [ViewVariables]
- private ThavenMoodsMenu? _menu;
-
- public ThavenMoodsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- }
-
- protected override void Open()
- {
- base.Open();
-
- _menu = this.CreateWindow();
- }
-
- protected override void UpdateState(BoundUserInterfaceState state)
- {
- base.UpdateState(state);
-
- if (state is not ThavenMoodsBuiState msg)
- return;
-
- if (!_entMan.TryGetComponent(Owner, out var comp))
- return;
-
- _menu?.Update(comp, msg);
- }
-}
diff --git a/Content.Client/_Mono/Audio/AudioEffectSystem.cs b/Content.Client/_Mono/Audio/AudioEffectSystem.cs
new file mode 100644
index 0000000000000..f6290cc0e61ae
--- /dev/null
+++ b/Content.Client/_Mono/Audio/AudioEffectSystem.cs
@@ -0,0 +1,260 @@
+// SPDX-FileCopyrightText: 2025 LaCumbiaDelCoronavirus
+// SPDX-FileCopyrightText: 2025 ark1368
+//
+// SPDX-License-Identifier: MPL-2.0
+
+// some parts taken and modified from https://github.com/TornadoTechnology/finster/blob/1af5daf6270477a512ee9d515371311443e97878/Content.Shared/_Finster/Audio/SharedAudioEffectsSystem.cs#L13 , credit to docnite
+// they're under WTFPL so its quite allowed
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Content.Shared.GameTicking;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
+
+namespace Content.Client._Mono.Audio;
+
+///
+/// Handler for client-side audio effects.
+///
+public sealed class AudioEffectSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+
+ ///
+ /// Whether creating new auxiliaries is safe.
+ /// This is done because integration tests
+ /// apparently can't handle them.
+ ///
+ /// Null means this value wasn't determined yet.
+ ///
+ /// Any not-null value here is the final result,
+ /// and no more attempts to determine this
+ /// will be made afterwards.
+ ///
+ // actually this problem applies for effects too
+ private bool? _auxiliariesSafe = null;
+
+ private static readonly Dictionary, (EntityUid AuxiliaryUid, EntityUid EffectUid)> CachedEffects = new();
+
+ ///
+ /// An auxiliary with no effect; for removing effects.
+ ///
+ // TODO: remove this when an rt method to actually remove effects gets added
+ private EntityUid _cachedBlankAuxiliaryUid;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // You can't keep references to this past round-end so it must be cleaned up.
+ SubscribeNetworkEvent(_ => Cleanup()); // its not raised on client
+ SubscribeLocalEvent(OnPrototypeReload);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ Cleanup();
+ }
+
+ private void OnPrototypeReload(PrototypesReloadedEventArgs args)
+ {
+ if (!args.WasModified())
+ return;
+
+ // get rid of all old cached entities, and replace them with new ones
+ var oldPresets = new List>();
+ foreach (var cache in CachedEffects)
+ {
+ oldPresets.Add(cache.Key);
+
+ TryQueueDel(cache.Value.AuxiliaryUid);
+ TryQueueDel(cache.Value.EffectUid);
+ }
+ CachedEffects.Clear();
+
+ foreach (var oldPreset in oldPresets)
+ {
+ if (!ResolveCachedEffect(oldPreset, out var cachedAuxiliaryUid, out var cachedEffectUid))
+ continue;
+
+ CachedEffects[oldPreset] = (cachedAuxiliaryUid.Value, cachedEffectUid.Value);
+ }
+ }
+
+ private void Cleanup()
+ {
+ foreach (var cache in CachedEffects)
+ {
+ TryQueueDel(cache.Value.AuxiliaryUid);
+ TryQueueDel(cache.Value.EffectUid);
+ }
+ CachedEffects.Clear();
+
+ if (_cachedBlankAuxiliaryUid.IsValid())
+ TryQueueDel(_cachedBlankAuxiliaryUid);
+
+ _cachedBlankAuxiliaryUid = EntityUid.Invalid;
+ }
+
+
+ ///
+ /// Figures out whether auxiliaries are safe to use. Returns
+ /// whether a safe auxiliary pair has been outputted
+ /// for use.
+ ///
+ private bool DetermineAuxiliarySafety([NotNullWhen(true)] out (EntityUid Entity, AudioAuxiliaryComponent Component)? auxiliaryPair, bool destroyPairAfterUse = true)
+ {
+ (EntityUid Entity, AudioAuxiliaryComponent Component)? maybeAuxiliaryPair = null;
+ try
+ {
+ maybeAuxiliaryPair = _audioSystem.CreateAuxiliary();
+ _auxiliariesSafe = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Info($"Determined audio auxiliaries are unsafe in this run! If this is not an integration test, report this immediately. Exception: {ex}");
+ _auxiliariesSafe = false;
+
+ TryQueueDel(maybeAuxiliaryPair?.Entity);
+
+ auxiliaryPair = null;
+ return false;
+ }
+
+ if (destroyPairAfterUse)
+ {
+ QueueDel(maybeAuxiliaryPair.Value.Entity);
+
+ auxiliaryPair = null;
+ return false;
+ }
+
+ auxiliaryPair = maybeAuxiliaryPair.Value;
+ return true;
+ }
+
+ ///
+ /// Returns whether auxiliaries are definitely safe to use.
+ /// Determines auxiliary safety if not already.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool AuxiliariesAreDefinitelySafe()
+ {
+ if (_auxiliariesSafe == null)
+ DetermineAuxiliarySafety(out _, destroyPairAfterUse: true);
+
+ return _auxiliariesSafe == true;
+ }
+
+ ///
+ /// Tries to resolve a cached audio auxiliary entity corresponding to the prototype to apply
+ /// to the given entity.
+ ///
+ public bool TryAddEffect(in Entity entity, in ProtoId preset)
+ {
+ if (!AuxiliariesAreDefinitelySafe() ||
+ !ResolveCachedEffect(preset, out var auxiliaryUid, out _))
+ return false;
+
+ _audioSystem.SetAuxiliary(entity, entity.Comp, auxiliaryUid);
+ return true;
+ }
+
+ ///
+ /// Tries to remove effects from the given audio. Returns whether the attempt was successful,
+ /// or no auxiliary is applied to the audio.
+ ///
+ public bool TryRemoveEffect(in Entity entity)
+ {
+ if (!AuxiliariesAreDefinitelySafe())
+ return false;
+
+ if (entity.Comp.Auxiliary is not { } existingAuxiliaryUid ||
+ !existingAuxiliaryUid.IsValid())
+ return true;
+
+ // resolve the cached auxiliary
+ if (!_cachedBlankAuxiliaryUid.IsValid())
+ _cachedBlankAuxiliaryUid = _audioSystem.CreateAuxiliary().Entity;
+
+ _audioSystem.SetAuxiliary(entity, entity.Comp, _cachedBlankAuxiliaryUid);
+ return true;
+ }
+
+ ///
+ /// Tries to resolve an audio auxiliary and effect entity, creating and caching one if one doesn't already exist,
+ /// for a prototype. Do not modify it in any way.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ResolveCachedEffect(in ProtoId preset, [NotNullWhen(true)] out EntityUid? auxiliaryUid, [NotNullWhen(true)] out EntityUid? effectUid)
+ {
+ if (_auxiliariesSafe == false)
+ {
+ auxiliaryUid = null;
+ effectUid = null;
+ return false;
+ }
+
+ if (CachedEffects.TryGetValue(preset, out var cached))
+ {
+ auxiliaryUid = cached.AuxiliaryUid;
+ effectUid = cached.EffectUid;
+ return true;
+ }
+
+ return TryCacheEffect(preset, out auxiliaryUid, out effectUid);
+ }
+
+ ///
+ /// Tries to initialise and cache effect and auxiliary entities corresponding to a prototype,
+ /// in the system's internal cache.
+ ///
+ /// Does nothing if the entity already exists in the cache.
+ ///
+ /// Whether the entity was successfully initialised, and it did not previously exist in the cache.
+ public bool TryCacheEffect(in ProtoId preset, [NotNullWhen(true)] out EntityUid? auxiliaryUid, [NotNullWhen(true)] out EntityUid? effectUid)
+ {
+
+ effectUid = null;
+ auxiliaryUid = null;
+
+ if (_auxiliariesSafe == false ||
+ !_prototypeManager.TryIndex(preset, out var presetPrototype))
+ return false;
+
+ // i cant `??=` it
+ (EntityUid Entity, AudioAuxiliaryComponent Component)? maybeAuxiliaryPair = null;
+
+ // if undetermined, determine and keep the pair if confirmed safe
+ if (_auxiliariesSafe == null)
+ {
+ // if determined unsafe, cleanup the pair
+ if (!DetermineAuxiliarySafety(out maybeAuxiliaryPair, destroyPairAfterUse: false))
+ return false;
+ }
+
+ // now, auxiliaries are known to be safe.
+ // only when initially determining if auxiliaries are safe will we have a pair to use. in future attempts, we won't so just make one if necessary
+ var auxiliaryPair = maybeAuxiliaryPair ?? _audioSystem.CreateAuxiliary();
+
+ DebugTools.Assert(Exists(auxiliaryPair.Entity), "Audio auxiliary pair's entity does not exist!");
+ if (!Exists(auxiliaryPair.Entity))
+ return false;
+
+ var effectPair = _audioSystem.CreateEffect();
+ _audioSystem.SetEffectPreset(effectPair.Entity, effectPair.Component, presetPrototype);
+ _audioSystem.SetEffect(auxiliaryPair.Entity, auxiliaryPair.Component, effectPair.Entity);
+
+ effectUid = effectPair.Entity;
+ auxiliaryUid = auxiliaryPair.Entity;
+
+ return CachedEffects.TryAdd(preset, (auxiliaryPair.Entity, effectPair.Entity));
+ }
+}
diff --git a/Content.Client/_VDS/Audio/AcousticDataSystem.cs b/Content.Client/_VDS/Audio/AcousticDataSystem.cs
new file mode 100644
index 0000000000000..c5c72cbe5614e
--- /dev/null
+++ b/Content.Client/_VDS/Audio/AcousticDataSystem.cs
@@ -0,0 +1,559 @@
+// SPDX-FileCopyrightText: 2025 LaCumbiaDelCoronavirus
+// SPDX-FileCopyrightText: 2025 ark1368
+// SPDX-FileCopyrightText: 2025 Jellvisk
+//
+// SPDX-License-Identifier: MPL-2.0
+
+// this has been heavily refactored by Jellvisk to the point
+// where this is like a ship of theseus situation.
+
+using Content.Client._Mono.Audio;
+using Content.Client._VDS.Audio.Components;
+using Content.Shared.Coordinates;
+using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
+using Content.Shared.Maps;
+using Content.Shared.Physics;
+using Content.Shared._VDS.Audio.Components;
+using Content.Shared._VDS.CCVars;
+using Content.Shared._VDS.Physics;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Configuration;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client._VDS.Audio;
+
+///
+/// Gathers environmental acoustic data around the player, later to be processed by .
+///
+public sealed class AcousticDataSystem : EntitySystem
+{
+ [Dependency] private readonly AudioEffectSystem _audioEffectSystem = default!;
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly ILogManager _logMan = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ReflectiveRaycastSystem _reflectiveRaycast = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly SharedRoofSystem _roofSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly TurfSystem _turfSystem = default!;
+
+ ///
+ /// Quick reference to our
+ ///
+ private AcousticSettingsComponent _acousticSettings = default!;
+
+ ///
+ /// The directions that are raycasted.
+ /// Used relative to the grid.
+ ///
+ private Angle[] _calculatedDirections = [Direction.North.ToAngle(), Direction.West.ToAngle(), Direction.South.ToAngle(), Direction.East.ToAngle()];
+
+ /* - VDS
+ TODO: this could be expanded to be more than just these few presets. see ReverbPresets.cs in Robust.Shared/Audio/Effects/
+ would require gathering more data.
+ */
+ ///
+ /// Arbitrary values for determining what ReverbPreset to use.
+ /// Defined in .
+ /// See .
+ ///
+ private SortedList>? _acousticPresets;
+
+ ///
+ /// The client's local entity, to spawn our raycasts at.
+ ///
+ private EntityUid _clientEnt;
+
+ private bool _acousticEnabled = true;
+
+ ///
+ /// Max amount of times single acoustic ray is allowed to bounce
+ ///
+ private int _acousticMaxReflections;
+
+ ///
+ /// Our previously recorded magnitude, for lerp purposes.
+ ///
+ private float _prevAvgMagnitude;
+
+
+ private EntityQuery _acousticQuery;
+ private EntityQuery _gridQuery;
+ private EntityQuery _roofQuery;
+ private EntityQuery _transformQuery;
+
+ private ISawmill _sawmill = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = _logMan.GetSawmill("acoustics");
+
+ _configurationManager.OnValueChanged(VCCVars.AcousticEnable, x => _acousticEnabled = x, invokeImmediately: true);
+ _configurationManager.OnValueChanged(VCCVars.AcousticHighResolution, x => _calculatedDirections = GetEffectiveDirections(x), invokeImmediately: true);
+ _configurationManager.OnValueChanged(VCCVars.AcousticReflectionCount, x => _acousticMaxReflections = x, invokeImmediately: true);
+
+ _acousticQuery = GetEntityQuery();
+ _gridQuery = GetEntityQuery();
+ _roofQuery = GetEntityQuery();
+ _transformQuery = GetEntityQuery();
+
+ /*
+ this is kinda janky as fuck. it also wasn't me who originally did it i swear
+ but to be fair it works good enough and I can't think of any other solution right
+ now and i'm tired good night
+ */
+ SubscribeLocalEvent(OnParentChange);
+
+ SubscribeLocalEvent(OnLocalPlayerAttached);
+ SubscribeLocalEvent(OnLocalPlayerDetached);
+ }
+
+ private void OnLocalPlayerAttached(LocalPlayerAttachedEvent ev)
+ {
+ _clientEnt = ev.Entity;
+ EnsureComp(_clientEnt, out var comp);
+ _acousticPresets = comp.ReverbPresets;
+ _acousticSettings = comp;
+ }
+
+ private void OnLocalPlayerDetached(LocalPlayerDetachedEvent ev)
+ {
+ RemComp(_clientEnt);
+ _clientEnt = EntityUid.Invalid;
+ }
+
+ private void OnParentChange(Entity audio, ref EntParentChangedMessage ev)
+ {
+ if (!CanAudioBePostProcessed(audio, ev.Transform))
+ return;
+
+ ProcessAcoustics(audio);
+ }
+
+ ///
+ /// Cast, get, and process the obtained .
+ ///
+ private void ProcessAcoustics(Entity audioEnt)
+ {
+ if (_acousticPresets == null || _acousticPresets.Count == 0)
+ return;
+
+ var maxMagnitude = _acousticPresets.Keys[^1];
+ var minMagnitude = _acousticPresets.Keys[0];
+
+ var magnitude = 0f;
+ if (TryCastAndGetEnvironmentAcousticData(
+ in _clientEnt,
+ in maxMagnitude,
+ in _acousticMaxReflections,
+ in _calculatedDirections,
+ out var acousticResults))
+ {
+ magnitude = CalculateAmplitude(
+ (_clientEnt, Transform(_clientEnt)),
+ in acousticResults);
+ }
+
+ if (magnitude > minMagnitude)
+ {
+ var bestPreset = GetBestReverbPreset(magnitude, _acousticPresets);
+ _audioEffectSystem.TryAddEffect(in audioEnt, in bestPreset);
+ }
+ else
+ {
+ _audioEffectSystem.TryRemoveEffect(in audioEnt);
+ }
+ }
+
+ ///
+ /// Basic check for whether an audio entity can be applied effects such as reverb.
+ ///
+ public bool CanAudioBePostProcessed(Entity audio, in TransformComponent xForm)
+ {
+ if (!_acousticEnabled)
+ return false;
+
+ // we cast from the player, so they need a valid entity.
+ if (!_clientEnt.IsValid())
+ return false;
+
+ if (TerminatingOrDeleted(audio))
+ return false;
+
+ // we only care about loaded local audio. it would be kinda weird
+ // if stuff like nukie music reverbed
+ if (!audio.Comp.Playing
+ || audio.Comp.Global
+ || audio.Comp.State == AudioState.Stopped)
+ {
+ return false;
+ }
+
+ Vector2 audioPos;
+ Vector2 clientPos;
+ if ((audio.Comp.Flags & AudioFlags.GridAudio) != 0x0)
+ {
+ audioPos = xForm.LocalPosition;
+ clientPos = _mapSystem.GetGridPosition(_clientEnt);
+ }
+ else
+ {
+ audioPos = _transformSystem.GetWorldPosition(xForm);
+ clientPos = _transformSystem.GetWorldPosition(_clientEnt);
+ }
+
+ // check distance!
+ var delta = audioPos - clientPos;
+ var distance = delta.Length();
+ return _audioSystem.GetAudioDistance(distance) <= audio.Comp.MaxDistance;
+ }
+
+ ///
+ /// Compares our magnitude to and returns the best match.
+ ///
+ [Pure]
+ public static ProtoId GetBestReverbPreset(float magnitude, SortedList> presetList)
+ {
+ var keys = presetList.Keys;
+ var index = keys.ToList().BinarySearch(magnitude);
+
+ // our magnitude was found exactly in the list so just take it i guess.
+ if (index >= 0)
+ return presetList.GetValueAtIndex(index);
+
+ // invert the bits to get our insertion point
+ index = ~index;
+ var lowerIndex = index - 1;
+ var upperIndex = index;
+
+ // edge cases
+ if (upperIndex == 0) // magnitude is smaller than the first element of our list
+ return presetList.GetValueAtIndex(upperIndex);
+ else if (lowerIndex == presetList.Count - 1) // magnitude is bigger than the last element of our list
+ return presetList.GetValueAtIndex(lowerIndex);
+
+ // return the value of whatever is closest to our magnitude
+ var lowerDiff = MathF.Abs(magnitude - keys[lowerIndex]);
+ var upperDiff = MathF.Abs(magnitude - keys[upperIndex]);
+ return (lowerDiff <= upperDiff) ? presetList.GetValueAtIndex(lowerIndex) : presetList.GetValueAtIndex(upperIndex);
+ }
+
+ ///
+ /// Returns all four cardinal directions when is false.
+ /// Otherwise, returns all eight intercardinal and cardinal directions as listed in
+ /// .
+ ///
+ [Pure]
+ public static Angle[] GetEffectiveDirections(bool highResolution)
+ {
+ if (highResolution)
+ {
+ var allDirections = DirectionExtensions.AllDirections;
+ var directions = new Angle[allDirections.Length];
+
+ for (var i = 0; i < allDirections.Length; i++)
+ directions[i] = allDirections[i].ToAngle();
+
+ return directions;
+ }
+
+ return [Direction.North.ToAngle(), Direction.West.ToAngle(), Direction.South.ToAngle(), Direction.East.ToAngle()];
+ }
+
+ ///
+ /// Attempts to cast and gather environmental around .
+ ///
+ ///
+ /// The origin of our raycasts.
+ /// Maximum range of a ray.
+ /// How many times a ray is allowed to bounce before terminating early.
+ /// What angles our rays will shoot out from.
+ /// A list of .
+ /// True if has data, false if is null or empty.
+ public bool TryCastAndGetEnvironmentAcousticData(
+ in EntityUid originEnt,
+ in float maxRange,
+ in int maxBounces,
+ in Angle[] castDirections,
+ [NotNullWhen(true)] out List? acousticResults)
+ {
+ acousticResults = new List(castDirections.Length);
+
+ if (!originEnt.IsValid()
+ || !_transformQuery.HasComponent(originEnt))
+ {
+ return false;
+ }
+
+ // in space nobody can hear your awesome freaking acoustics
+ if (!_turfSystem.TryGetTileRef(originEnt.ToCoordinates(), out var tileRef)
+ || _turfSystem.IsSpace(tileRef.Value))
+ {
+ return false;
+ }
+
+ var clientTransform = Transform(originEnt);
+ var clientMapId = clientTransform.MapID;
+ var clientCoords = _transformSystem.ToMapCoordinates(clientTransform.Coordinates).Position;
+
+ // our path filter, which will return AcousticDataComponent entities our ray passes through
+ var pathFilter = new QueryFilter
+ {
+ MaskBits = (int)CollisionGroup.AllMask,
+ IsIgnored = ent => !_acousticQuery.HasComp(ent), // ideally we'd pass _absorptionQuery via state, but the new ray system doesn't allow that for some reason
+ Flags = QueryFlags.Static | QueryFlags.Dynamic
+ };
+
+ // our probe filter, which determines what our rays will bounce off of.
+ var probeFilter = new QueryFilter
+ {
+ MaskBits = (int)CollisionGroup.AllMask,
+ LayerBits = (int)CollisionGroup.None,
+ IsIgnored = ent => _acousticQuery.TryGetComponent(ent, out var comp) && !comp.ReflectRay,
+ Flags = QueryFlags.Static | QueryFlags.Dynamic
+ };
+
+ // our current ray state, which is passed through and altered by ref.
+ // instead of making states for each ray we will just reuse one and reset it
+ // before passing it back in for the next direction. for performance or whatever.
+ var state = new ReflectiveRayState(
+ probeFilter,
+ pathFilter,
+ origin: clientCoords,
+ direction: Vector2.Zero, // we change the dir later
+ maxRange: maxRange,
+ clientMapId
+ );
+
+ // cast our rays and get our results
+ acousticResults = CastManyReflectiveAcousticRays(
+ in originEnt,
+ clientCoords,
+ in maxBounces,
+ in castDirections,
+ ref state);
+
+ return acousticResults.Count != 0;
+ }
+
+ ///
+ /// Casts many bouncing rays.
+ ///
+ ///
+ ///
+ public List CastManyReflectiveAcousticRays(
+ in EntityUid originEnt,
+ Vector2 originCoords,
+ in int maxBounces,
+ in Angle[] castDirections,
+ ref ReflectiveRayState state)
+ {
+ var acousticResults = new List();
+
+ foreach (var direction in castDirections)
+ {
+ state.CurrentPos = originCoords;
+ state.OldPos = originCoords;
+ state.Direction = (direction + _random.NextFloat(
+ -_acousticSettings.DirectionRandomOffset,
+ _acousticSettings.DirectionRandomOffset)).ToVec();
+ state.Translation = state.Direction * state.MaxRange;
+ state.ProbeTranslation = state.Translation;
+ state.RemainingDistance = state.MaxRange;
+
+
+ // handle individual bounces
+ var results = CastReflectiveAcousticRay(
+ in originEnt,
+ in maxBounces,
+ ref state);
+ acousticResults.Add(results);
+ }
+
+ return acousticResults;
+ }
+
+ ///
+ /// Casts a bouncing ray.
+ ///
+ ///
+ /// The entity to compare absorption falloff to.
+ /// , in order hit, including whatever the ray bounced off.
+ public AcousticRayResults CastReflectiveAcousticRay(
+ in EntityUid originEnt,
+ in int maxBounces,
+ ref ReflectiveRayState state)
+ {
+ var results = new AcousticRayResults();
+ for (var bounce = 0; bounce <= maxBounces; bounce++)
+ {
+ /*
+ our raycast state will constantly be fed by reference into the reflective raycast API,
+ which updates the reference's positional data for us, including the handling of
+ bounces with each iteration (provided we pass the ref back through).
+ we also get a new list of entities for each iteration so we
+ can do component data gathering on them.
+ */
+ var (probeResult, pathResults) = _reflectiveRaycast.CastAndUpdateReflectiveRayStateRef(ref state);
+
+ results.TotalRange += state.CurrentSegmentDistance;
+ if (probeResult.Hit)
+ {
+ pathResults.Results.Add(probeResult.Results[0]); // we wanna include what we hit to our data too
+ results.TotalBounces++;
+ }
+
+ // gather acoustic component data
+ if (pathResults.Results.Count > 0)
+ {
+ foreach (var result in pathResults.Results)
+ {
+ if (!_acousticQuery.TryGetComponent(result.Entity, out var comp))
+ continue;
+
+ // TODO: more component data can be gathered here in the future
+ results.TotalAbsorption += GetAcousticAbsorption(
+ result,
+ in originEnt,
+ in comp);
+ }
+ }
+
+ // this ray is long enough to be considered in an open area and now shall be ignored
+ if (state.CurrentSegmentDistance >= state.MaxRange * _acousticSettings.EscapeDistancePercentage)
+ {
+ results.TotalEscapes++;
+ break;
+ }
+
+ // expended our range budget, break the loop
+ if (results.TotalRange >= state.MaxRange)
+ break;
+ }
+ return results;
+ }
+
+ ///
+ /// Gets an absorption percentage using inverse square falloff.
+ ///
+ private float GetAcousticAbsorption(
+ RayHit result,
+ in EntityUid originEnt,
+ in AcousticDataComponent comp)
+ {
+ result.Entity.ToCoordinates().TryDistance(
+ EntityManager,
+ originEnt.ToCoordinates(),
+ out var distance);
+
+ // make sure we don't divide by zero.
+ var distanceSquared = MathF.Max(distance * distance, 0.01f);
+
+ return (comp.Absorption < 0)
+ ? -NormalizeToPercentage(comp.Absorption, -100f, 0f, maxClamp: _acousticSettings.MaxAbsorptionClamp) * distanceSquared
+ : NormalizeToPercentage(comp.Absorption, maxClamp: _acousticSettings.MaxAbsorptionClamp) * distanceSquared;
+ }
+
+
+ ///
+ /// Calculates our the overall amplitude of .
+ ///
+ /// Where the rays originally came from, for roof detecting purposes.
+ /// Our ray's amplitude
+ private float CalculateAmplitude(
+ Entity originEnt,
+ in List acousticResults)
+ {
+ var totalRays = acousticResults.Count;
+ var avgMagnitude = acousticResults.Average(mag => mag.TotalRange);
+ var avgAbsorption = acousticResults.Average(absorb => absorb.TotalAbsorption);
+ var escaped = acousticResults.Sum(escapees => escapees.TotalEscapes);
+ // TODO: resonance??
+ // var avgBounces = (float)acousticResults.Average(bounce => bounce.TotalBounces);
+
+ // we store our previous avg magnitude and lerp it with the current to make sure changes aren't too jarring
+ if (_prevAvgMagnitude > float.Epsilon)
+ avgMagnitude = MathHelper.Lerp(_prevAvgMagnitude, avgMagnitude, _acousticSettings.AvgMagnitudeBlend);
+ _prevAvgMagnitude = avgMagnitude;
+
+ var amplitude = 0f;
+ var absorbMultiplier = InverseNormalizeToPercentage(avgAbsorption, maxClamp: _acousticSettings.MaxAbsorptionClamp); // things like furniture or different material walls should eat our energy
+ var escapeMultiplier = MathF.Max(InverseNormalizeToPercentage(escaped, 0f, totalRays), _acousticSettings.MaxmimumEscapePenalty); // escaped rays are mostly irrelevant, so penalize based on that.
+
+ amplitude += avgMagnitude;
+ amplitude *= absorbMultiplier;
+ amplitude *= escapeMultiplier;
+
+ // severely punish our amplitude if there is no roof.
+ if (originEnt.Comp.GridUid.HasValue
+ && _roofQuery.TryGetComponent(originEnt.Comp.GridUid.Value, out var roof)
+ && _gridQuery.TryGetComponent(originEnt.Comp.GridUid.Value, out var grid)
+ && _transformSystem.TryGetGridTilePosition(originEnt.Owner, out var indices)
+ && !_roofSystem.IsRooved((originEnt.Comp.GridUid.Value, grid, roof), indices))
+ {
+ amplitude *= _acousticSettings.NoRoofPenalty;
+ }
+
+ // _sawmill.Debug($"""
+ // Results:
+ // Absorbtion Multiplier: {absorbMultiplier:F3}
+ // Escape Multiplier: {escapeMultiplier:F3}
+ // Final Amplitude: {amplitude:F3}
+ // Acoustic Preset: {GetBestReverbPreset(amplitude, _acousticPresets!)}
+ // """);
+
+ return amplitude;
+ }
+
+ ///
+ /// Returns a 0f..1f percent, where the closer to 0f the value is, the closer to 100% (1.0f) it is.
+ ///
+ public static float NormalizeToPercentage(
+ float value,
+ float minValue = 0f,
+ float maxValue = 100f,
+ float maxClamp = 1f)
+ {
+ var percentage = (value - minValue) / (maxValue - minValue);
+ return Math.Clamp(percentage, 0f, maxClamp);
+ }
+
+ ///
+ /// Returns a 0f..1f percent, where the closer to 1.0f the value is, the closer to 0% (0f) it is.
+ ///
+ public static float InverseNormalizeToPercentage(
+ float value,
+ float minValue = 0f,
+ float maxValue = 100f,
+ float maxClamp = 1f)
+ {
+ return NormalizeToPercentage(maxValue - value, minValue, maxValue, maxClamp);
+ }
+
+ ///
+ /// Data about the current acoustic environment and relevant variables.
+ ///
+ public struct AcousticRayResults
+ {
+ public float TotalAbsorption;
+ public int TotalBounces;
+ public int TotalEscapes;
+ public float TotalRange;
+ }
+}
diff --git a/Content.Client/_VDS/Audio/Components/AcousticSettingsComponent.cs b/Content.Client/_VDS/Audio/Components/AcousticSettingsComponent.cs
new file mode 100644
index 0000000000000..822000e7c7755
--- /dev/null
+++ b/Content.Client/_VDS/Audio/Components/AcousticSettingsComponent.cs
@@ -0,0 +1,72 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._VDS.Audio.Components;
+
+///
+/// Holds client-side settings for that the player
+/// should not be able to normally adjust.
+///
+[RegisterComponent]
+[Access(typeof(AcousticDataSystem))]
+public sealed partial class AcousticSettingsComponent : Component
+{
+ ///
+ /// A list of distances and what to use alongside it.
+ ///
+ [DataField, ViewVariables]
+ public SortedList> ReverbPresets = new()
+ {
+ { 10f, "SpaceStationCupboard" },
+ { 13f, "DustyRoom" },
+ { 15f, "SpaceStationSmallRoom" },
+ { 18f, "SpaceStationShortPassage" },
+ { 23f, "SpaceStationMediumRoom" },
+ { 28f, "SpaceStationHall" },
+ { 35f, "SpaceStationLargeRoom" },
+ { 40f, "Auditorium" },
+ { 45f, "ConcertHall" },
+ { 70f, "Hangar" },
+ };
+
+ ///
+ /// Based on the maximum posssible distance an acoustic raycast can travel,
+ /// what percentage a single segment of it can it travel before it is considered 'escaped' and terminated early?
+ ///
+ [DataField, ViewVariables]
+ public float EscapeDistancePercentage = 0.3f;
+
+ ///
+ /// We will never penalize our acoustic data less than this percentage.
+ ///
+ [DataField, ViewVariables]
+ public float MaxmimumEscapePenalty = 0.10f;
+
+ ///
+ /// Penalize the all of the acoustic data by this percentage if the client is standing in
+ /// an unrooved area.
+ ///
+ [DataField, ViewVariables]
+ public float NoRoofPenalty = 0.10f;
+
+ ///
+ /// Maximum random degree offset an acoustic ray may take each bounce.
+ /// Note that this is applied both clock-wise and counter-clockwise.
+ ///
+ [DataField, ViewVariables]
+ public float DirectionRandomOffset = 0.3f;
+
+ ///
+ /// How large our absorption modifier is allowed to get.
+ /// Values above 1.0f allow negative values
+ /// to amplify the acoustic magnitude.
+ ///
+ [DataField, ViewVariables]
+ public float MaxAbsorptionClamp = 1.3f;
+
+ ///
+ /// How much blending we do via lerp for our previous and current average magnitude values.
+ ///
+ [DataField, ViewVariables]
+ public float AvgMagnitudeBlend = 0.25f;
+}
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 5ac4f83431056..857cbb3aa3e62 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -21,7 +21,6 @@ public sealed class EntityTest
private static readonly ProtoId SpawnerCategory = "Spawner";
[Test]
- [Explicit] // Floofstation - OOM bait
public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps()
{
// This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round
@@ -85,7 +84,6 @@ await server.WaitPost(() =>
}
[Test]
- [Explicit] // Floofstation - OOM bait
public async Task SpawnAndDeleteAllEntitiesInTheSameSpot()
{
// This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round
@@ -145,7 +143,6 @@ await server.WaitPost(() =>
/// all components on every entity.
///
[Test]
- [Explicit] // Floofstation - OOM bait
[Ignore("Broken due to engine issue relating to RemCompDeferred")] // imp heisentest
public async Task SpawnAndDirtyAllEntities()
{
diff --git a/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs
index 7a5195800ed67..de7cd3eb542f7 100644
--- a/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs
+++ b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs
@@ -58,6 +58,18 @@ public async Task HungerThirstIncreaseDecreaseTest()
// We eat the food in hand
await UseInHand();
+ // IMP START: we removed eating doafter auto-looping so you have to eat it more to actually delete it. sorry
+ var fullyEaten = false;
+ while (fullyEaten == false)
+ {
+ await AwaitDoAfters();
+ await UseInHand();
+
+ if (HandSys.GetActiveItem((SPlayer, Hands)) != null)
+ fullyEaten = true;
+ }
+ // IMP END
+
// To see a change in hunger, we need to wait at least 30 seconds
await RunSeconds(30);
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 0f48afc96c75c..27ca2b751f0a8 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -169,16 +169,13 @@ private static readonly (string, string)[] IgnoreUnmappedSpawns =
"E1M1",
"ElkridgeImp",
"GateImp",
- "reHash",
"Hummingbird",
"Lilboat",
- "Luna",
"MarathonImp",
"OasisImp",
"PackedImp",
"PlasmaImp",
"ReachImp",
- "RelicImp",
"SalternImp",
"Submarine",
"TrainImp",
@@ -187,11 +184,13 @@ private static readonly (string, string)[] IgnoreUnmappedSpawns =
"Whisper",
"Monarch",
- // NOT IN ROTATION BUT WE STILL NEED THEM TESTED SINCE THEY STILL HAVE A PROTOTYPE:
- "Eclipse",
- "Refsdal",
- "Skimmer",
- "Union",
+ // DEROTATED:
+ //"Eclipse",
+ //"Luna",
+ //"Refsdal",
+ //"reHash",
+ //"RelicImp",
+ //"Skimmer",
};
private static readonly ProtoId DoNotMapCategory = "DoNotMap";
diff --git a/Content.IntegrationTests/Tests/_Impstation/StrangeMoods/MoodTests.cs b/Content.IntegrationTests/Tests/_Impstation/StrangeMoods/MoodTests.cs
new file mode 100644
index 0000000000000..41803ffc8c18d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/_Impstation/StrangeMoods/MoodTests.cs
@@ -0,0 +1,91 @@
+using System.Linq;
+using Content.Server._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared.Dataset;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests._Impstation.StrangeMoods;
+
+[TestFixture, TestOf(typeof(StrangeMoodPrototype))]
+public sealed class StrangeMoodTests
+{
+ [TestPrototypes]
+ private const string Prototypes = """
+
+ - type: dataset
+ id: ThreeValueSet
+ values:
+ - One
+ - Two
+ - Three
+ - type: strangeMood
+ id: DuplicateTest
+ moodName: DuplicateTest
+ moodDesc: DuplicateTest
+ allowDuplicateMoodVars: false
+ moodVars:
+ a: ThreeValueSet
+ b: ThreeValueSet
+ c: ThreeValueSet
+ - type: strangeMood
+ id: DuplicateOverlapTest
+ moodName: DuplicateOverlapTest
+ moodDesc: DuplicateOverlapTest
+ allowDuplicateMoodVars: false
+ moodVars:
+ a: ThreeValueSet
+ b: ThreeValueSet
+ c: ThreeValueSet
+ d: ThreeValueSet
+ e: ThreeValueSet
+
+ """;
+
+ [Test]
+ [Repeat(10)]
+ public async Task TestDuplicatePrevention()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+ await server.WaitIdleAsync();
+
+ var entMan = server.ResolveDependency();
+ var moodSystem = entMan.System();
+ var protoMan = server.ResolveDependency();
+
+ var dataset = protoMan.Index("ThreeValueSet");
+ var moodProto = protoMan.Index("DuplicateTest");
+
+ var datasetSet = dataset.Values.ToHashSet();
+ var mood = moodSystem.RollMood(moodProto);
+ var moodVarSet = mood.MoodVars.Values.ToHashSet();
+
+ Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ [Repeat(10)]
+ public async Task TestDuplicateOverlap()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var moodSystem = entMan.System();
+ var protoMan = server.ResolveDependency();
+
+ var dataset = protoMan.Index("ThreeValueSet");
+ var moodProto = protoMan.Index("DuplicateOverlapTest");
+
+ var datasetSet = dataset.Values.ToHashSet();
+ var mood = moodSystem.RollMood(moodProto);
+ var moodVarSet = mood.MoodVars.Values.ToHashSet();
+
+ Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/_Impstation/Thaven/MoodTests.cs b/Content.IntegrationTests/Tests/_Impstation/Thaven/MoodTests.cs
deleted file mode 100644
index b2a189b850182..0000000000000
--- a/Content.IntegrationTests/Tests/_Impstation/Thaven/MoodTests.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using Content.IntegrationTests;
-using Content.Server._Impstation.Thaven;
-using Content.Shared.Dataset;
-using Content.Shared._Impstation.Thaven;
-using NUnit.Framework;
-using Robust.Shared.ContentPack;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.Manager;
-
-namespace Content.IntegrationTests.Tests._Impstation.Thaven;
-
-[TestFixture, TestOf(typeof(ThavenMoodPrototype))]
-public sealed class ThavenMoodTests
-{
- [TestPrototypes]
- const string PROTOTYPES = @"
-- type: dataset
- id: ThreeValueSet
- values:
- - One
- - Two
- - Three
-- type: thavenMood
- id: DuplicateTest
- moodName: DuplicateTest
- moodDesc: DuplicateTest
- allowDuplicateMoodVars: false
- moodVars:
- a: ThreeValueSet
- b: ThreeValueSet
- c: ThreeValueSet
-- type: thavenMood
- id: DuplicateOverlapTest
- moodName: DuplicateOverlapTest
- moodDesc: DuplicateOverlapTest
- allowDuplicateMoodVars: false
- moodVars:
- a: ThreeValueSet
- b: ThreeValueSet
- c: ThreeValueSet
- d: ThreeValueSet
- e: ThreeValueSet
-";
-
- [Test]
- [Repeat(10)]
- public async Task TestDuplicatePrevention()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
- await server.WaitIdleAsync();
-
- var entMan = server.ResolveDependency();
- var thavenSystem = entMan.System();
- var protoMan = server.ResolveDependency();
-
- var dataset = protoMan.Index("ThreeValueSet");
- var moodProto = protoMan.Index("DuplicateTest");
-
- var datasetSet = dataset.Values.ToHashSet();
- var mood = thavenSystem.RollMood(moodProto);
- var moodVarSet = mood.MoodVars.Values.ToHashSet();
-
- Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
-
- await pair.CleanReturnAsync();
- }
-
- [Test]
- [Repeat(10)]
- public async Task TestDuplicateOverlap()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.ResolveDependency();
- var thavenSystem = entMan.System();
- var protoMan = server.ResolveDependency();
-
- var dataset = protoMan.Index("ThreeValueSet");
- var moodProto = protoMan.Index("DuplicateOverlapTest");
-
- var datasetSet = dataset.Values.ToHashSet();
- var mood = thavenSystem.RollMood(moodProto);
- var moodVarSet = mood.MoodVars.Values.ToHashSet();
-
- Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
-
- await pair.CleanReturnAsync();
- }
-}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index 274fbd6badc29..2977eb23653ed 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -1,6 +1,7 @@
using Content.Server.Antag;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server._Harmony.GameTicking.Rules.Components; //harmony
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Server.Clothing.Systems;
@@ -31,6 +32,7 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultThiefRule = "Thief";
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
+ private static readonly EntProtoId DefaultConspiratorRule = "Conspirators"; // Harmony
private static readonly EntProtoId DefaultWizardRule = "Wizard";
private static readonly EntProtoId DefaultNinjaRule = "NinjaSpawn";
private static readonly ProtoId PirateGearId = "PirateGear";
@@ -241,5 +243,22 @@ private void AddAntagVerbs(GetVerbsEvent args)
Message = Loc.GetString("admin-verb-make-heretic"),
};
args.Verbs.Add(heretic);
+
+ // Harmony start
+ var conspiratorName = Loc.GetString("admin-verb-text-make-conspirator");
+ Verb conspirator = new()
+ {
+ Text = conspiratorName,
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Rsi(new("/Textures/_Harmony/Interface/Misc/job_icons.rsi"), "Conspirator"),
+ Act = () =>
+ {
+ _antag.ForceMakeAntag(targetPlayer, DefaultConspiratorRule);
+ },
+ Impact = LogImpact.High,
+ Message = string.Join(": ", conspiratorName, Loc.GetString("admin-verb-make-conspirator")),
+ };
+ args.Verbs.Add(conspirator);
+ // Harmony end
}
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
index 5e720c61f99c4..7996fc603895d 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
@@ -36,10 +36,12 @@
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
+using Content.Server._Impstation.StrangeMoods.Eui; // imp
+using Content.Shared._Impstation.StrangeMoods; // imp
using Content.Server.Revenant.Components; // imp
using Content.Server.Revenant.EntitySystems; // imp
-using Content.Shared._Impstation.Thaven.Components; // imp
using Content.Shared.Item; // imp
+using Robust.Shared.Random; // imp
namespace Content.Server.Administration.Systems;
@@ -774,8 +776,11 @@ private void AddTricksVerbs(GetVerbsEvent args)
args.Verbs.Add(makeInanimate);
}
- if (TryComp(args.Target, out var moods))
+ if (TryComp(args.Target, out var moods))
{
+ if (moods.StrangeMood.Datasets.Count <= 0)
+ return;
+
Verb addRandomMood = new()
{
Text = "Add Random Mood",
@@ -783,7 +788,7 @@ private void AddTricksVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new ResPath("Interface/Actions/actions_borg.rsi"), "state-laws"),
Act = () =>
{
- _moods.TryAddRandomMood((args.Target, moods));
+ _moods.TryAddRandomMood((args.Target, moods), _random.Pick(moods.StrangeMood.Datasets).Key);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-trick-add-random-mood-description"),
@@ -800,13 +805,15 @@ private void AddTricksVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new ResPath("Interface/Actions/actions_borg.rsi"), "state-laws"),
Act = () =>
{
- if (!EnsureComp(args.Target, out moods))
- {
- //if we're adding moods to something that doesn't already have them (e.g. isn't a thaven), make them ignore the shared mood
- var targ = (args.Target, moods);
- _moods.SetMoods(targ, []);
- _moods.SetFollowsSharedmood(targ, false);
- }
+ if (HasComp(args.Target))
+ return;
+
+ var ui = new StrangeMoodsInitEui(_moods, EntityManager, _prototypeManager, _random, _adminManager, _playerManager, _euiManager, args.User);
+ if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
+ return;
+
+ _euiManager.OpenEui(ui, session);
+ ui.SetTarget(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-trick-give-moods-description"),
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs
index 61f0e8a4a2313..99b1ebda2aa83 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs
@@ -35,9 +35,10 @@
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
+using Content.Server._Impstation.StrangeMoods; // imp
+using Content.Server._Impstation.StrangeMoods.Eui; // imp
+using Content.Shared._Impstation.StrangeMoods; // imp
using static Content.Shared.Configurable.ConfigurationComponent;
-using Content.Shared._Impstation.Thaven.Components; // imp
-using Content.Server._Impstation.Thaven; // imp
namespace Content.Server.Administration.Systems
{
@@ -68,7 +69,7 @@ public sealed partial class AdminVerbSystem : EntitySystem
[Dependency] private readonly AdminFrozenSystem _freeze = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SiliconLawSystem _siliconLawSystem = default!;
- [Dependency] private readonly ThavenMoodsSystem _moods = default!; // imp
+ [Dependency] private readonly StrangeMoodsSystem _moods = default!; // imp
private readonly Dictionary> _openSolutionUis = new();
@@ -410,15 +411,15 @@ private void AddAdminVerbs(GetVerbsEvent args)
});
// Begin Impstation Additions
- if (TryComp(args.Target, out var moods))
+ if (TryComp(args.Target, out var moods))
{
args.Verbs.Add(new Verb()
{
- Text = Loc.GetString("thaven-moods-ui-verb"),
+ Text = Loc.GetString("strange-moods-ui-verb"),
Category = VerbCategory.Admin,
Act = () =>
{
- var ui = new ThavenMoodsEui(_moods, EntityManager, _adminManager);
+ var ui = new StrangeMoodsEui(_moods, EntityManager, _random, _adminManager);
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
return;
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
index 662720c9c3c8b..0297484a627b7 100644
--- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
@@ -11,6 +11,7 @@
using Robust.Shared.Timing;
using System.Text;
using Content.Shared._DV.NanoChat; // dv
+using Content.Shared.PDA; // imp
namespace Content.Server.CartridgeLoader.Cartridges;
@@ -54,6 +55,23 @@ private void AfterInteract(Entity ent, ref Cartridge
return;
}
// DeltaV end
+ // IMP ADD- more nanochat card scanning
+ else if (TryComp(target, out var pda))
+ {
+ if (pda.ContainedId is { } pdaId
+ && TryComp(pdaId, out var pdaNanoChatCard))
+ {
+ ScanNanoChatCard(ent, args, pdaId, pdaNanoChatCard);
+ }
+ else
+ {
+ _popup.PopupCursor(Loc.GetString("log-probe-scan-nanochat-empty-pda", ("pda", target)), args.InteractEvent.User);
+ }
+
+ args.InteractEvent.Handled = true;
+ return;
+ }
+ // imp end
if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
return;
diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
index 3b19bbffb6aee..bdd41dce058a5 100644
--- a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
@@ -27,4 +27,31 @@ public sealed partial class RevolutionaryRuleComponent : Component
///
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan ShuttleCallTime = TimeSpan.FromMinutes(5);
+
+ //imp datafields below
+
+ ///
+ /// The threshold of converted players that will prompt an automatic alert level change.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float BlueThreshold = 0.3f;
+
+ ///
+ /// The alert level triggered by a threshold of players being converted.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string AlertLevel = "blue";
+
+ ///
+ /// The announcement id triggered by a threshold of players being converted.
+ /// This is not the string itself, but the ID, e.g. it will search for station-event-sleeper-agents
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string RuleId = "sleeper-agents";
+
+ ///
+ /// Whether the game rule has already triggered a conversion announcement.
+ ///
+ [DataField]
+ public bool AnnouncementDone = false;
}
diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
index d2cd27022fb1d..75a04b9ebf1a8 100644
--- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
@@ -30,6 +30,8 @@
using Robust.Shared.Timing;
using Content.Shared.Cuffs.Components;
using Robust.Shared.Player;
+using Content.Server.AlertLevel; // imp edit
+using Content.Server.Announcements.Systems; // imp edit
namespace Content.Server.GameTicking.Rules;
@@ -52,6 +54,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem RevolutionaryNpcFaction = "Revolutionary";
@@ -177,6 +181,17 @@ private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref Aft
if (mind is { UserId: not null } && _player.TryGetSessionById(mind.UserId, out var session))
_antag.SendBriefing(session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
+ // imp start
+ foreach (var rule in GameTicker.GetActiveGameRules())
+ {
+ if (TryComp(rule, out var ruleComp))
+ {
+ if (!ruleComp.AnnouncementDone)
+ AnnounceConverts(ruleComp); //check if we've met the threshold for blue alert announcement & do it
+ return;
+ }
+ }
+ // imp end
}
//TODO: Enemies of the revolution
@@ -317,4 +332,44 @@ private bool IsGroupDetainedOrDead(List list, bool checkOffStation, b
// revs lost and heads died
"rev-stalemate"
};
+ //imp start
+ ///
+ /// If crew conversion falls at or above the threshold, sends an announcement and changes the alert level, nonrepeatable.
+ ///
+ private void AnnounceConverts(RevolutionaryRuleComponent component)
+ {
+ if (CheckCrewConversion() >= component.BlueThreshold)
+ {
+ _announcer.SendAnnouncement(
+ _announcer.GetAnnouncementId(component.RuleId), // announce parameters identical to sleepers for obfuscation
+ Filter.Broadcast(),
+ _announcer.GetEventLocaleString(_announcer.GetAnnouncementId(component.RuleId)),
+ colorOverride: Color.Gold
+ );
+ component.AnnouncementDone = true; // gamerule remembers that the announcement already happened this round
+ if (!TryGetRandomStation(out var chosenStation))
+ return;
+ if (_alertLevelSystem.GetLevel(chosenStation.Value) != "green") // no blue alert if already pink or red
+ return;
+ _alertLevelSystem.SetLevel(chosenStation.Value, component.AlertLevel, true, true, true);
+ }
+ }
+ ///
+ /// Checks what fraction of players are converted to revolutionaries.
+ ///
+ private float CheckCrewConversion()
+ {
+ float converted = 0;
+ float crew = 0;
+ var players = AllEntityQuery(); //determining players roughly how zombies does, humanoids with a player controlling them
+ var revs = GetEntityQuery();
+ while (players.MoveNext(out var uid, out _, out _))
+ {
+ if (revs.HasComponent(uid))
+ converted++; //if they have revcomp, count them as converted. should count head revs
+ crew++;
+ }
+ return converted / crew;
+ }
+ //imp end
}
diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs
index ee3817944d42e..0a8fbbb569e4d 100644
--- a/Content.Server/Store/Systems/StoreSystem.Listings.cs
+++ b/Content.Server/Store/Systems/StoreSystem.Listings.cs
@@ -130,6 +130,11 @@ public IEnumerable GetAvailableListings(
{
if (!condition.Condition(args))
{
+ // imp edit start
+ if (condition.MakeHidden)
+ listing.HiddenWhenUnbuyable = true;
+ // imp edit end
+
conditionsMet = false;
break;
}
@@ -141,6 +146,9 @@ public IEnumerable GetAvailableListings(
listing.Buyable = false;
if (listing.Priority < 1000)
listing.Priority += 1000;
+
+ if (!listing.Buyable && listing.HiddenWhenUnbuyable)
+ continue;
// imp end
}
}
diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs
index 7c5c99b5b4fbe..4a176ad19f305 100644
--- a/Content.Server/Store/Systems/StoreSystem.cs
+++ b/Content.Server/Store/Systems/StoreSystem.cs
@@ -11,6 +11,8 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using Content.Shared.Power.Components; // imp add
+using Content.Shared.Power.EntitySystems; // imp add
namespace Content.Server.Store.Systems;
@@ -23,6 +25,9 @@ public sealed partial class StoreSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedBatterySystem _battery = default!; // imp
+
+ private readonly static string UnpoweredPopup = "store-currency-not-charged"; // imp add
public override void Initialize()
{
@@ -100,6 +105,15 @@ private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterIn
if (ev.Cancelled)
return;
+ // IMP ADD: you cannot refund items that need charging unless they are fully charged!
+ if (TryComp(uid, out var battery) &&
+ _battery.GetCharge(uid) < battery.MaxCharge)
+ {
+ _popup.PopupCursor(Loc.GetString(UnpoweredPopup), args.User);
+ return;
+ }
+ // IMP END
+
if (!TryAddCurrency((uid, component), (args.Target.Value, store)))
return;
diff --git a/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs b/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs
index dc72a084990ff..25222f7166822 100644
--- a/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs
+++ b/Content.Server/StoreDiscount/Systems/StoreDiscountSystem.cs
@@ -255,6 +255,11 @@ IEnumerable listings
continue;
}
+ // imp edit start, unbuyable listings can't be discounted
+ if (!listing.Buyable)
+ continue;
+ // imp edit end
+
if (!listingsByDiscountCategory.TryGetValue(category.Value, out var list))
{
list = new List();
diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs
index 1c283783937f5..189bd3b6d661b 100644
--- a/Content.Server/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Server/VendingMachines/VendingMachineSystem.cs
@@ -13,6 +13,8 @@
using Content.Shared.Wall;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Content.Server.Store.Systems; // IMP ADD
+using Content.Shared.Store.Components; // IMP ADD
namespace Content.Server.VendingMachines
{
@@ -21,6 +23,8 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly StoreSystem _store = default!; // IMP ADD
+
private const float WallVendEjectDistanceFromWall = 1f;
@@ -36,6 +40,8 @@ public override void Initialize()
SubscribeLocalEvent(OnSelfDispense);
SubscribeLocalEvent(OnPriceCalculation);
+
+ SubscribeLocalEvent(OnUiMessage); // IMP ADD
}
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
@@ -244,5 +250,12 @@ private void OnTryVocalize(Entity ent, ref TryVocalizeE
{
args.Cancelled |= ent.Comp.Broken;
}
+
+ // IMP ADD
+ private void OnUiMessage(Entity ent, ref VendingStoreOpenMessage msg)
+ {
+ if (HasComp(ent))
+ _store.ToggleUi(msg.Actor, ent);
+ }
}
}
diff --git a/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.Processing.cs b/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.Processing.cs
index ddd903015738d..8395f3695ffa3 100644
--- a/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.Processing.cs
+++ b/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.Processing.cs
@@ -2,12 +2,11 @@
using System.Numerics;
using System.Text;
using Content.Server.Chat.Systems;
-using Content.Server.Light.Components;
using Content.Server.Singularity.Components;
using Content.Server.StationEvents.Events;
using Content.Shared._EE.CCVar;
using Content.Shared._EE.Supermatter.Components;
-using Content.Shared._Impstation.Thaven.Components;
+using Content.Shared._Impstation.StrangeMoods;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.Chat;
@@ -681,8 +680,11 @@ private void HandleDelamination(EntityUid uid, SupermatterComponent sm)
_entityLookup.GetEntitiesOnMap(mapId, mobLookup);
mobLookup.RemoveWhere(x => HasComp(x));
- // Scramble the thaven shared mood
- _moods.NewSharedMoods();
+ // Scramble the given shared moods
+ foreach (var mood in sm.SharedMoodScrambleTargets)
+ {
+ _moods.NewSharedMoods(mood);
+ }
// Flickers all powered lights on the map
var lightLookup = new HashSet>();
@@ -702,9 +704,13 @@ private void HandleDelamination(EntityUid uid, SupermatterComponent sm)
foreach (var mob in mobLookup)
{
- // Scramble thaven moods
- if (TryComp(mob, out var moods))
+ // Scramble moods that follow the given shared moods
+ if (TryComp(mob, out var moods) &&
+ moods.SharedMood is { UniqueId: not null } sharedMood &&
+ sm.SharedMoodScrambleTargets.Contains(sharedMood.UniqueId))
+ {
_moods.RefreshMoods((mob, moods));
+ }
// Scramble laws for silicons, then ignore other effects
if (TryComp(mob, out var law))
diff --git a/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.cs b/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.cs
index e2e2323b72416..db8b12851c52b 100644
--- a/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.cs
+++ b/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.cs
@@ -1,4 +1,4 @@
-using Content.Server._Impstation.Thaven;
+using Content.Server._Impstation.StrangeMoods;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
@@ -61,7 +61,7 @@ public sealed partial class SupermatterSystem : EntitySystem
[Dependency] private readonly PointLightSystem _light = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly RadioSystem _radio = default!;
- [Dependency] private readonly ThavenMoodsSystem _moods = default!;
+ [Dependency] private readonly StrangeMoodsSystem _moods = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambient = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
diff --git a/Content.Server/_Goobstation/Heretic/Abilities/HereticAbilitySystem.Flesh.cs b/Content.Server/_Goobstation/Heretic/Abilities/HereticAbilitySystem.Flesh.cs
index c456f9633ffd5..09f2c0c3ddd61 100644
--- a/Content.Server/_Goobstation/Heretic/Abilities/HereticAbilitySystem.Flesh.cs
+++ b/Content.Server/_Goobstation/Heretic/Abilities/HereticAbilitySystem.Flesh.cs
@@ -142,7 +142,7 @@ private void OnFleshAscendPolymorph(Entity ent, ref EventHeret
Color eyeColor;
Color bloodColor;
if (TryComp(entity, out var humanoid) && TryComp(entity, out var bloodstream) // get the humanoidappearance and bloodstream
- && bloodstream.BloodReferenceSolution.Contents[1].Reagent.Prototype is { } reagentProto // TODO: FIX THIS
+ && bloodstream.BloodReferenceSolution.Contents[0].Reagent.Prototype is { } reagentProto // TODO: FIX THIS
&& _prot.TryIndex(reagentProto, out ReagentPrototype? blood) && blood != null) // get the blood reagent
{
skinColor = humanoid.SkinColor;
diff --git a/Content.Server/_Harmony/GameTicking/Rules/Components/ConspiratorRuleComponent.cs b/Content.Server/_Harmony/GameTicking/Rules/Components/ConspiratorRuleComponent.cs
new file mode 100644
index 0000000000000..aec29f36c0392
--- /dev/null
+++ b/Content.Server/_Harmony/GameTicking/Rules/Components/ConspiratorRuleComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._Harmony.GameTicking.Rules.Components;
+
+///
+/// Game rule for conspirators. Handles their shared objective.
+///
+[RegisterComponent, Access(typeof(ConspiratorRuleSystem))]
+public sealed partial class ConspiratorRuleComponent : Component
+{
+ [DataField]
+ public EntProtoId? Objective = null;
+
+ [DataField]
+ public ProtoId ObjectiveGroup = "ConspiratorObjectiveGroup";
+}
diff --git a/Content.Server/_Harmony/GameTicking/Rules/ConspiratorRuleSystem.cs b/Content.Server/_Harmony/GameTicking/Rules/ConspiratorRuleSystem.cs
new file mode 100644
index 0000000000000..aa1a82f599b56
--- /dev/null
+++ b/Content.Server/_Harmony/GameTicking/Rules/ConspiratorRuleSystem.cs
@@ -0,0 +1,99 @@
+using Content.Server._Harmony.GameTicking.Rules.Components;
+using Content.Server.Antag;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
+using Content.Server.Roles;
+using Content.Shared._Harmony.Conspirators.Components;
+using Content.Shared._Harmony.Roles.Components;
+using Content.Shared.GameTicking.Components;
+using Content.Shared.Mind;
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Server._Harmony.GameTicking.Rules;
+
+public sealed class ConspiratorRuleSystem : GameRuleSystem
+{
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetBriefing);
+ SubscribeLocalEvent(OnAntagSelected);
+ }
+
+ protected override void AppendRoundEndText(EntityUid uid,
+ ConspiratorRuleComponent component,
+ GameRuleComponent gameRule,
+ ref RoundEndTextAppendEvent args)
+ {
+ base.AppendRoundEndText(uid, component, gameRule, ref args);
+
+ var sessionData = _antag.GetAntagIdentifiers(uid);
+ args.AddLine(Loc.GetString("conspirator-count", ("count", sessionData.Count)));
+ foreach (var (_, data, name) in sessionData)
+ {
+ args.AddLine(Loc.GetString("conspirator-name-user",
+ ("name", name),
+ ("username", data.UserName)));
+ }
+
+ if (!_proto.TryIndex(component.Objective, out var objectiveProto))
+ return;
+
+ args.AddLine(Loc.GetString("conspirator-objective", ("objective", objectiveProto.Name)));
+ }
+
+ private void OnGetBriefing(Entity ent, ref GetBriefingEvent args)
+ {
+ args.Append(Loc.GetString("conspirator-identities"));
+
+ var conspirators = AllEntityQuery();
+ while (conspirators.MoveNext(out var id, out _))
+ {
+ args.Append(Loc.GetString("conspirator-name", ("name", Name(id))));
+ }
+
+ args.Append(Loc.GetString("conspirator-radio-implant"));
+ }
+
+ private void OnAntagSelected(Entity ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
+ return;
+
+ if (ent.Comp.Objective is null)
+ {
+ if (GetRandomObjectivePrototype(ent.Comp, out var objectiveProtoId))
+ ent.Comp.Objective = objectiveProtoId;
+ }
+
+ if (ent.Comp.Objective is not null)
+ _mind.TryAddObjective(mindId, mind, ent.Comp.Objective);
+ }
+
+ private bool GetRandomObjectivePrototype(ConspiratorRuleComponent comp, [NotNullWhen(true)] out EntProtoId? objectiveProto)
+ {
+ objectiveProto = null;
+
+ if (!_proto.TryIndex(comp.ObjectiveGroup, out var group))
+ return false;
+
+ var objectives = group.Weights.ShallowClone();
+ while (_random.TryPickAndTake(objectives, out var proto))
+ {
+ objectiveProto = proto!;
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs b/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs
new file mode 100644
index 0000000000000..6497fe46533d5
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsEui.cs
@@ -0,0 +1,77 @@
+using Content.Server.Administration.Managers;
+using Content.Server.EUI;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using Robust.Shared.Player;
+
+namespace Content.Server._Impstation.StrangeMoods.Eui;
+
+public sealed class SharedMoodsEui(
+ StrangeMoodsSystem strangeMoods,
+ IAdminManager admin,
+ EuiManager eui,
+ ICommonSession user) : BaseEui
+{
+ private readonly ISawmill _sawmill = Logger.GetSawmill("strange-moods-eui");
+
+ private SharedMoodsInitEui? _sharedUi;
+ private string? _targetMood;
+
+ public override EuiStateBase GetNewState()
+ {
+ var sharedMoods = strangeMoods.GetSharedMoods();
+ var mood = _targetMood;
+ _targetMood = null;
+ return new SharedMoodsEuiState(sharedMoods, mood);
+ }
+
+ public void SetMood(string? mood)
+ {
+ _targetMood = mood;
+ StateDirty();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (!IsAllowed())
+ return;
+
+ switch (msg)
+ {
+ case SharedMoodsInitStartMessage:
+ {
+ _sharedUi = new SharedMoodsInitEui(strangeMoods);
+ _sharedUi.OnValidMessage += (allSharedMoods, mood) => SendMessage(new SharedMoodsInitValidMessage(allSharedMoods, mood));
+ eui.OpenEui(_sharedUi, user);
+ break;
+ }
+ case SharedMoodsSaveMessage saveData:
+ {
+ strangeMoods.NewSharedMoods(saveData.Target, saveData.Moods, true);
+ break;
+ }
+ case SharedMoodsRequestMessage requestData:
+ {
+ if (!strangeMoods.TryGetSharedMood(requestData.MoodId, out var mood))
+ return;
+
+ SendMessage(new SharedMoodsSendMessage(mood));
+ break;
+ }
+ }
+ }
+
+ private bool IsAllowed()
+ {
+ var adminData = admin.GetAdminData(Player);
+
+ if (adminData != null && adminData.HasFlag(AdminFlags.Moderator))
+ return true;
+
+ _sawmill.Warning($"Player {Player.UserId} tried to open / use strange moods UI without permission.");
+ return false;
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs b/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs
new file mode 100644
index 0000000000000..0a072dbda21e2
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/Eui/SharedMoodsInitEui.cs
@@ -0,0 +1,37 @@
+using System.Text.RegularExpressions;
+using Content.Server.EUI;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Eui;
+
+namespace Content.Server._Impstation.StrangeMoods.Eui;
+
+public sealed partial class SharedMoodsInitEui(StrangeMoodsSystem strangeMoods) : BaseEui
+{
+ [GeneratedRegex("^[A-Za-z]+$")]
+ private static partial Regex ProtoIdRegex();
+
+ public event Action, SharedMood>? OnValidMessage;
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not SharedMoodsInitAcceptMessage message)
+ return;
+
+ if (!ProtoIdRegex().IsMatch(message.Name) ||
+ strangeMoods.SharedMoodIdExists(message.Name))
+ {
+ SendMessage(new SharedMoodsInitErrorMessage());
+ return;
+ }
+
+ var mood = new SharedMood { UniqueId = message.Name, Count = 0 };
+ strangeMoods.NewSharedMoods(mood);
+
+ var allMoods = strangeMoods.GetSharedMoods();
+ SendMessage(new SharedMoodsInitValidMessage(allMoods, mood));
+ OnValidMessage?.Invoke(allMoods, mood);
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs b/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs
new file mode 100644
index 0000000000000..762c0a188e3aa
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsEui.cs
@@ -0,0 +1,112 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.Administration.Managers;
+using Content.Server.EUI;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using Robust.Shared.Random;
+
+namespace Content.Server._Impstation.StrangeMoods.Eui;
+
+public sealed class StrangeMoodsEui(
+ StrangeMoodsSystem strangeMoods,
+ EntityManager entity,
+ IRobustRandom random,
+ IAdminManager admin) : BaseEui
+{
+ private readonly ISawmill _sawmill = Logger.GetSawmill("strange-moods-eui");
+
+ private List _moods = [];
+ private SharedMood? _sharedMood;
+ private EntityUid _target;
+
+ public override EuiStateBase GetNewState()
+ {
+ var sharedMoods = strangeMoods.GetSharedMoods();
+ return new StrangeMoodsEuiState(sharedMoods, _moods, _sharedMood, entity.GetNetEntity(_target));
+ }
+
+ public void UpdateMoods(EntityUid uid, List moods, SharedMood? sharedMood)
+ {
+ if (!IsAllowed())
+ return;
+
+ _moods = moods;
+ _sharedMood = sharedMood;
+ _target = uid;
+
+ StateDirty();
+ }
+
+ public void UpdateMoods(Entity ent)
+ {
+ UpdateMoods(ent, ent.Comp.StrangeMood.Moods, ent.Comp.SharedMood);
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (!IsAllowed())
+ return;
+
+ switch (msg)
+ {
+ case StrangeMoodsSaveMessage saveData:
+ {
+ if (!HasStrangeMoods(saveData.Target, out var uid, out var comp))
+ return;
+
+ strangeMoods.SetSharedMood((uid, comp), saveData.SharedMoodId);
+ strangeMoods.SetMoods((uid, comp), saveData.Moods);
+ break;
+ }
+ case StrangeMoodsGenerateRequestMessage requestGenerateData:
+ {
+ if (!HasStrangeMoods(requestGenerateData.Target, out var uid, out var comp))
+ return;
+
+ var activeMoods = strangeMoods.GetActiveMoods((uid, comp));
+
+ if (comp.StrangeMood.Datasets.Count <= 0 ||
+ !strangeMoods.TryPick(random.Pick(comp.StrangeMood.Datasets).Key, out var moodProto, activeMoods))
+ return;
+
+ var newMood = strangeMoods.RollMood(moodProto);
+ SendMessage(new StrangeMoodsGenerateSendMessage(newMood));
+ break;
+ }
+ case StrangeMoodsSharedRequestMessage requestSharedData:
+ {
+ if (requestSharedData.MoodId is not { } id ||
+ !strangeMoods.TryGetSharedMood(id, out var mood))
+ return;
+
+ SendMessage(new StrangeMoodsSharedSendMessage(mood));
+ break;
+ }
+ }
+ }
+
+ private bool IsAllowed()
+ {
+ var adminData = admin.GetAdminData(Player);
+
+ if (adminData != null && adminData.HasFlag(AdminFlags.Moderator))
+ return true;
+
+ _sawmill.Warning($"Player {Player.UserId} tried to open / use strange moods UI without permission.");
+ return false;
+ }
+
+ private bool HasStrangeMoods(NetEntity ent, out EntityUid uid, [NotNullWhen(true)] out StrangeMoodsComponent? comp)
+ {
+ uid = entity.GetEntity(ent);
+ if (entity.TryGetComponent(uid, out comp))
+ return true;
+
+ _sawmill.Warning($"Entity {entity.ToPrettyString(uid)} does not have StrangeMoodsComponent!");
+ return false;
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs b/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs
new file mode 100644
index 0000000000000..0e258e6c54c96
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/Eui/StrangeMoodsInitEui.cs
@@ -0,0 +1,80 @@
+using Content.Server.Administration.Managers;
+using Content.Server.EUI;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared._Impstation.StrangeMoods.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using Robust.Server.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server._Impstation.StrangeMoods.Eui;
+
+public sealed class StrangeMoodsInitEui(
+ StrangeMoodsSystem strangeMoods,
+ EntityManager entity,
+ IPrototypeManager prototype,
+ IRobustRandom random,
+ IAdminManager admin,
+ IPlayerManager player,
+ EuiManager eui,
+ EntityUid user) : BaseEui
+{
+ private readonly ISawmill _sawmill = Logger.GetSawmill("strange-moods-init-eui");
+
+ private EntityUid _target;
+
+ public override EuiStateBase GetNewState()
+ {
+ return new StrangeMoodsInitEuiState(entity.GetNetEntity(_target));
+ }
+
+ public void SetTarget(EntityUid target)
+ {
+ _target = target;
+
+ StateDirty();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not StrangeMoodsInitAcceptMessage message)
+ return;
+
+ if (!IsAllowed())
+ return;
+
+ if (!player.TryGetSessionByEntity(user, out var session))
+ return;
+
+ var target = entity.GetEntity(message.Target);
+ var comp = StrangeMoodsSystem.CreateComponent(message.Definition);
+ var ui = new StrangeMoodsEui(strangeMoods, entity, random, admin);
+ SharedMood? sharedMood = null;
+
+ entity.AddComponent(target, comp);
+ var newComp = entity.GetComponent(target);
+ strangeMoods.RefreshMoods((target, newComp));
+
+ var moods = newComp.StrangeMood.Moods;
+
+ if (prototype.TryIndex(message.Definition.SharedMoodPrototype, out var sharedProto))
+ strangeMoods.TryGetSharedMood(sharedProto, out sharedMood);
+
+ eui.OpenEui(ui, session);
+ ui.UpdateMoods(target, moods, sharedMood);
+ }
+
+ private bool IsAllowed()
+ {
+ var adminData = admin.GetAdminData(Player);
+
+ if (adminData != null && adminData.HasFlag(AdminFlags.Moderator))
+ return true;
+
+ _sawmill.Warning($"Player {Player.UserId} tried to open / use strange moods init UI without permission.");
+ return false;
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/SharedMoodsCommand.cs b/Content.Server/_Impstation/StrangeMoods/SharedMoodsCommand.cs
new file mode 100644
index 0000000000000..3dfcbd966fe46
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/SharedMoodsCommand.cs
@@ -0,0 +1,42 @@
+using Content.Server._Impstation.StrangeMoods.Eui;
+using Content.Server.Administration;
+using Content.Server.Administration.Managers;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server._Impstation.StrangeMoods;
+
+[AdminCommand(AdminFlags.Fun)]
+public sealed class SharedMoodsCommand : LocalizedEntityCommands
+{
+ [Dependency] private readonly IAdminManager _admin = default!;
+ [Dependency] private readonly EuiManager _eui = default!;
+ [Dependency] private readonly StrangeMoodsSystem _strangeMoods = default!;
+
+ public override string Command => "sharedmoods";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (shell.Player is not { } player)
+ {
+ shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
+ return;
+ }
+
+ if (args.Length > 1)
+ {
+ shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
+ return;
+ }
+
+ string? mood = null;
+
+ if (args.Length == 1)
+ mood = args[0];
+
+ var ui = new SharedMoodsEui(_strangeMoods, _admin, _eui, player);
+ _eui.OpenEui(ui, player);
+ ui.SetMood(mood);
+ }
+}
diff --git a/Content.Server/_Impstation/StrangeMoods/StrangeMoodsSystem.cs b/Content.Server/_Impstation/StrangeMoods/StrangeMoodsSystem.cs
new file mode 100644
index 0000000000000..5b9caff0e8fc0
--- /dev/null
+++ b/Content.Server/_Impstation/StrangeMoods/StrangeMoodsSystem.cs
@@ -0,0 +1,550 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Actions;
+using Content.Server.Chat.Managers;
+using Content.Shared._Impstation.StrangeMoods;
+using Content.Shared.Chat;
+using Content.Shared.Dataset;
+using Content.Shared.GameTicking;
+using Content.Shared.Random;
+using Content.Shared.Random.Helpers;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization.Manager;
+
+namespace Content.Server._Impstation.StrangeMoods;
+
+public sealed class StrangeMoodsSystem : SharedStrangeMoodsSystem
+{
+ [Dependency] private readonly IChatManager _chat = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
+ [Dependency] private readonly ActionsSystem _actions = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly UserInterfaceSystem _bui = default!;
+
+ private readonly HashSet _sharedMoods = [];
+ private readonly HashSet _emptyMoods = []; // cached hashset that never gets modified
+ private readonly HashSet> _moodProtos = []; // cached hashset that gets changed in GetMoodProtoSet
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ NewSharedMoods();
+
+ SubscribeLocalEvent(OnStrangeMoodsInit);
+ SubscribeLocalEvent(OnStrangeMoodsShutdown);
+ SubscribeLocalEvent(OnToggleMoodsScreen);
+ SubscribeLocalEvent(OnBoundUIOpened);
+ SubscribeLocalEvent(_ => NewSharedMoods());
+ }
+
+ private void OnStrangeMoodsInit(Entity ent, ref MapInitEvent args)
+ {
+ var mood = ent.Comp.StrangeMood;
+
+ if (_proto.TryIndex(ent.Comp.StrangeMoodPrototype, out var moodProto))
+ {
+ _serialization.CopyTo(moodProto, ref mood, notNullableOverride: true);
+
+ // Add any required components
+ if (moodProto.Components is { } components)
+ {
+ EntityManager.AddComponents(ent, components);
+ }
+ }
+
+ RefreshMoods(ent);
+ SetSharedMood(ent, mood.SharedMoodPrototype);
+
+ // Add action to bar
+ ent.Comp.Action ??= _actions.AddAction(ent.Owner, mood.ActionViewMoods);
+ if (TryComp(ent, out var ui))
+ _bui.SetUi((ent, ui), StrangeMoodsUiKey.Key, new InterfaceData("StrangeMoodsBoundUserInterface", requireInputValidation: false));
+ }
+
+ private void OnStrangeMoodsShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ // Remove action
+ _actions.RemoveAction(ent.Owner, ent.Comp.Action);
+
+ if (!_proto.TryIndex(ent.Comp.StrangeMoodPrototype, out var moodProto))
+ return;
+
+ // Remove any added components
+ if (moodProto.Components is { } components)
+ {
+ EntityManager.RemoveComponents(ent, components);
+ }
+ }
+
+ private void OnToggleMoodsScreen(Entity ent, ref ToggleMoodsScreenEvent args)
+ {
+ if (args.Handled || !TryComp(ent, out var actor) ||
+ !_bui.TryToggleUi(ent.Owner, StrangeMoodsUiKey.Key, actor.PlayerSession))
+ return;
+
+ args.Handled = true;
+ }
+
+ private void OnBoundUIOpened(Entity ent, ref BoundUIOpenedEvent args)
+ {
+ UpdateBuiState(ent);
+ }
+
+ #region Helper functions
+
+ ///
+ /// Clears the moods for an entity, then applies a new set of moods.
+ ///
+ public void RefreshMoods(Entity ent, Dictionary, int>? datasets = null)
+ {
+ datasets ??= ent.Comp.StrangeMood.Datasets;
+ ent.Comp.StrangeMood.Moods = _emptyMoods.ToList();
+
+ foreach (var moodset in datasets)
+ {
+ var dataset = moodset.Key;
+ var count = moodset.Value;
+
+ for (var i = 0; i < count; i++)
+ {
+ if (TryPick(dataset, out var mood, GetActiveMoods(ent)))
+ TryAddMood(ent, mood, true, false);
+ }
+ }
+ }
+
+ ///
+ /// Checks if the given mood prototype conflicts with the current moods, and
+ /// adds the mood if it does not.
+ ///
+ public bool TryAddMood(Entity ent, StrangeMoodPrototype moodProto, bool allowConflict = false, bool notify = true)
+ {
+ if (!allowConflict && GetConflicts(ent).Contains(moodProto.ID))
+ return false;
+
+ AddMood(ent, RollMood(moodProto), notify);
+ return true;
+ }
+
+ ///
+ /// Tries to add a random mood using a specific dataset.
+ ///
+ public bool TryAddRandomMood(Entity ent, ProtoId dataset, bool notify = true, HashSet>? conflicts = null)
+ {
+ conflicts ??= [];
+ conflicts.UnionWith(GetConflicts(ent));
+
+ if (!TryPick(dataset, out var moodProto, GetActiveMoods(ent), conflicts))
+ return false;
+
+ AddMood(ent, RollMood(moodProto), notify);
+ return true;
+ }
+
+ ///
+ /// Tries to add a random mood using a weighted random dataset.
+ ///
+ public bool TryAddRandomMood(Entity ent, ProtoId dataset, bool notify = true, HashSet>? conflicts = null)
+ {
+ var chosenDataset = _proto.Index(dataset).Pick();
+
+ if (!_proto.Resolve(chosenDataset, out DatasetPrototype? proto))
+ return false;
+
+ return TryAddRandomMood(ent, proto, notify, conflicts);
+ }
+
+ ///
+ /// Set the moods for an entity directly.
+ /// This does NOT check conflicts so be careful with what you set!
+ ///
+ public void SetMoods(Entity ent, IEnumerable moods, bool notify = true)
+ {
+ ent.Comp.StrangeMood.Moods = moods.ToList();
+ Dirty(ent);
+
+ if (notify)
+ NotifyMoodChange(ent);
+ else
+ UpdateBuiState(ent);
+ }
+
+ ///
+ /// Sends the player an audiovisual notification and updates the moods UI.
+ ///
+ public void NotifyMoodChange(Entity ent)
+ {
+ if (!TryComp(ent, out var actor))
+ return;
+
+ var session = actor.PlayerSession;
+ _audio.PlayGlobal(ent.Comp.StrangeMood.MoodsChangedSound, session);
+
+ var msg = Loc.GetString(ent.Comp.StrangeMood.MoodsChangedMessage);
+ var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
+ _chat.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, session.Channel, colorOverride: ent.Comp.StrangeMood.MoodsChangedColor);
+
+ UpdateBuiState(ent); // update the UI without needing to re-open it
+ }
+
+ ///
+ /// Directly add a mood to an entity, ignoring conflicts.
+ ///
+ private void AddMood(Entity ent, StrangeMood mood, bool notify = true)
+ {
+ ent.Comp.StrangeMood.Moods.Add(mood);
+ Dirty(ent);
+
+ if (notify)
+ NotifyMoodChange(ent);
+ else // NotifyMoodChange will update UI so this is in else
+ UpdateBuiState(ent);
+ }
+
+ ///
+ /// Attempts to pick a new mood from the given dataset.
+ ///
+ public bool TryPick(ProtoId? dataset, [NotNullWhen(true)] out StrangeMoodPrototype? proto, IEnumerable? currentMoods = null, HashSet>? conflicts = null)
+ {
+ if (!_proto.TryIndex(dataset, out var datasetProto))
+ {
+ proto = null;
+ return false;
+ }
+
+ var choices = datasetProto.Values.ToList();
+
+ currentMoods ??= _emptyMoods;
+ conflicts ??= GetConflicts(currentMoods);
+
+ var currentMoodProtos = GetMoodProtoSet(currentMoods);
+
+ while (choices.Count > 0)
+ {
+ var moodId = _random.PickAndTake(choices);
+
+ if (conflicts.Contains(moodId))
+ continue; // Skip proto if an existing mood conflicts with it
+
+ var moodProto = _proto.Index(moodId);
+
+ if (moodProto.Conflicts.Overlaps(currentMoodProtos))
+ continue; // Skip proto if it conflicts with an existing mood
+
+ proto = moodProto;
+ return true;
+ }
+
+ proto = null;
+ return false;
+ }
+
+ ///
+ /// Attempts to get the relevant shared mood from .
+ ///
+ public bool TryGetSharedMood(string id, out SharedMood? sharedMood)
+ {
+ foreach (var mood in _sharedMoods.Where(mood => id == mood.UniqueId))
+ {
+ sharedMood = mood;
+ return true;
+ }
+
+ sharedMood = null;
+ return false;
+ }
+
+ ///
+ /// Attempts to get a 's relevant shared mood from .
+ ///
+ public bool TryGetSharedMood(SharedMoodPrototype proto, out SharedMood? sharedMood)
+ {
+ if (proto.UniqueId != null)
+ return TryGetSharedMood(proto.UniqueId, out sharedMood);
+
+ sharedMood = null;
+ return false;
+
+ }
+
+ ///
+ /// Updates the mood UI.
+ ///
+ private void UpdateBuiState(Entity ent)
+ {
+ var sharedMoods = ent.Comp.SharedMood is { } sharedMood
+ ? sharedMood.Moods
+ : [];
+
+ var moods = ent.Comp.StrangeMood.Moods;
+
+ var state = new StrangeMoodsBuiState(sharedMoods, moods);
+ _bui.SetUiState(ent.Owner, StrangeMoodsUiKey.Key, state);
+ }
+
+ ///
+ /// Creates a StrangeMood instance from the given StrangeMoodPrototype, and rolls
+ /// its mood vars.
+ ///
+ public StrangeMood RollMood(StrangeMoodPrototype proto)
+ {
+ var mood = new StrangeMood();
+ _serialization.CopyTo(proto, ref mood, notNullableOverride: true);
+ var alreadyChosen = new HashSet>();
+
+ foreach (var (name, datasetId) in proto.MoodVarDatasets)
+ {
+ var dataset = _proto.Index(datasetId);
+
+ if (proto.AllowDuplicateMoodVars)
+ {
+ mood.MoodVars.Add(name, _random.Pick(dataset.Values));
+ continue;
+ }
+
+ var choices = dataset.Values.ToList();
+ var foundChoice = false;
+
+ while (choices.Count > 0)
+ {
+ var choice = _random.PickAndTake(choices);
+ if (alreadyChosen.Contains(choice) || mood.MoodVars.ContainsValue(choice))
+ continue;
+
+ mood.MoodVars.TryAdd(name, choice);
+ alreadyChosen.Add(choice);
+ foundChoice = true;
+ break;
+ }
+
+ if (!foundChoice)
+ Log.Warning($"Ran out of choices for moodvar \"{name}\" in \"{proto.ID}\"!");
+ }
+
+ return mood;
+ }
+
+ ///
+ /// Get the conflicts for an entity's active moods.
+ ///
+ private static HashSet> GetConflicts(IEnumerable moods)
+ {
+ var conflicts = new HashSet>();
+
+ foreach (var mood in moods)
+ {
+ if (mood.ProtoId is { } id)
+ conflicts.Add(id); // Specific moods shouldn't be added twice
+
+ conflicts.UnionWith(mood.Conflicts);
+ }
+
+ return conflicts;
+ }
+
+ ///
+ /// Get the conflicts for an entity's active moods.
+ ///
+ private HashSet> GetConflicts(Entity ent)
+ {
+ // TODO: Should probably cache this when moods get updated
+ return GetConflicts(GetActiveMoods(ent));
+ }
+
+ ///
+ /// Maps some moods to their IDs.
+ /// The hashset returned is reused and so you must not modify it.
+ ///
+ private HashSet> GetMoodProtoSet(IEnumerable moods)
+ {
+ _moodProtos.Clear();
+
+ foreach (var mood in moods)
+ {
+ if (mood.ProtoId is { } id)
+ _moodProtos.Add(id);
+ }
+
+ return _moodProtos;
+ }
+
+ ///
+ /// Returns the server's shared moods.
+ ///
+ public HashSet GetSharedMoods()
+ {
+ return _sharedMoods;
+ }
+
+ ///
+ /// Return a list of the moods that are affecting this entity.
+ ///
+ public List GetActiveMoods(Entity ent, bool includeShared = true)
+ {
+ if (includeShared && ent.Comp.SharedMood is { } sharedMood)
+ {
+ return [..sharedMood.Moods.Concat(ent.Comp.StrangeMood.Moods)];
+ }
+
+ return ent.Comp.StrangeMood.Moods;
+ }
+
+ ///
+ /// Creates a from a .
+ ///
+ public static StrangeMoodsComponent CreateComponent(StrangeMoodDefinition definition)
+ {
+ var newComp = new StrangeMoodsComponent
+ {
+ StrangeMood = definition,
+ StrangeMoodPrototype = definition.ProtoId,
+ };
+
+ return newComp;
+ }
+
+ #endregion
+
+ #region Shared mood helper functions
+
+ ///
+ /// Generates new moods for all shared moods.
+ ///
+ private void NewSharedMoods()
+ {
+ _sharedMoods.Clear();
+
+ var sharedMoods = _proto.EnumeratePrototypes();
+
+ foreach (var mood in sharedMoods)
+ {
+ TryAddSharedMood(mood);
+ }
+ }
+
+ ///
+ /// Generates new moods for the given shared mood.
+ /// Sets moods from a preset list if given a .
+ ///
+ public void NewSharedMoods(SharedMood sharedMood, List? moodList = null, bool clearOldMoods = false)
+ {
+ _sharedMoods.Remove(sharedMood);
+
+ if (clearOldMoods)
+ sharedMood.Moods.Clear();
+
+ if (moodList != null)
+ TryAddSharedMood(sharedMood, moodList);
+ else
+ TryAddSharedMood(sharedMood);
+ }
+
+ ///
+ /// Generates new moods for the given shared mood.
+ /// Sets moods from a preset list if given a .
+ ///
+ public void NewSharedMoods(string sharedMood, List? moodList = null, bool clearOldMoods = false)
+ {
+ if (!TryGetSharedMood(sharedMood, out var mood) || mood == null)
+ return;
+
+ NewSharedMoods(mood, moodList, clearOldMoods);
+ }
+
+ ///
+ /// Attempts to add moods to the given shared mood.
+ /// If no moods are specified, a random mood is added.
+ ///
+ private bool TryAddSharedMood(SharedMood sharedMood, List? newMoods = null, bool checkConflicts = true, bool notify = true)
+ {
+ var mood = new SharedMood();
+ _serialization.CopyTo(sharedMood, ref mood, notNullableOverride: true);
+
+ if (newMoods == null)
+ {
+ newMoods = [];
+
+ for (var i = 0; i < mood.Count; i++)
+ {
+ if (!TryPick(mood.Dataset, out var moodProto, mood.Moods))
+ return false;
+
+ newMoods.Add(RollMood(moodProto));
+ checkConflicts = false; // TryPick has cleared this mood already
+ }
+ }
+
+ foreach (var newMood in newMoods)
+ {
+ if (checkConflicts && SharedMoodConflicts(mood, newMood))
+ return false;
+
+ mood.Moods.Add(newMood);
+ }
+
+ _sharedMoods.Add(mood);
+ SyncSharedMoodChange(mood, notify);
+ return true;
+ }
+
+ ///
+ /// Sets which shared moods an entity should follow.
+ /// If null, the entity will not follow any shared moods.
+ ///
+ public void SetSharedMood(Entity ent, string? newMood)
+ {
+ if (newMood == null ||
+ !TryGetSharedMood(newMood, out var sharedMood))
+ {
+ ent.Comp.SharedMood = null;
+ return;
+ }
+
+ ent.Comp.SharedMood = sharedMood;
+ }
+
+ ///
+ /// Determines whether a mood conflicts with the current moods in a .
+ ///
+ private bool SharedMoodConflicts(SharedMood sharedMood, StrangeMood mood)
+ {
+ return mood.ProtoId is { } id &&
+ (GetConflicts(sharedMood.Moods).Contains(id) ||
+ GetMoodProtoSet(sharedMood.Moods).Overlaps(mood.Conflicts));
+ }
+
+ ///
+ /// Syncs a "moods changed" alert to all entities with the same shared mood.
+ /// By default, also sends a "moods changed" alert to those entities.
+ ///
+ private void SyncSharedMoodChange(SharedMood sharedMood, bool notify = true)
+ {
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (comp.SharedMood == null ||
+ comp.SharedMood.UniqueId != sharedMood.UniqueId)
+ continue;
+
+ comp.SharedMood = sharedMood;
+
+ if (notify)
+ NotifyMoodChange((uid, comp));
+ }
+ }
+
+ public bool SharedMoodIdExists(string id)
+ {
+ return _sharedMoods.Any(mood => mood.UniqueId == id);
+ }
+
+ #endregion
+}
diff --git a/Content.Server/_Impstation/Thaven/ThavenMoodSystem.cs b/Content.Server/_Impstation/Thaven/ThavenMoodSystem.cs
deleted file mode 100644
index 4bdbd0b964c54..0000000000000
--- a/Content.Server/_Impstation/Thaven/ThavenMoodSystem.cs
+++ /dev/null
@@ -1,431 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Server.Actions;
-using Content.Server.Chat.Managers;
-using Content.Shared.CCVar;
-using Content.Shared.Chat;
-using Content.Shared.Dataset;
-using Content.Shared.Emag.Systems;
-using Content.Shared.GameTicking;
-using Content.Shared._Impstation.Thaven;
-using Content.Shared._Impstation.Thaven.Components;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
-using Robust.Server.GameObjects;
-using Robust.Shared.Configuration;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Content.Shared._Impstation.CCVar;
-using Robust.Shared.Audio.Systems;
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server._Impstation.Thaven;
-
-public sealed partial class ThavenMoodsSystem : SharedThavenMoodSystem
-{
- [Dependency] private readonly IPrototypeManager _proto = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ActionsSystem _actions = default!;
- [Dependency] private readonly IConfigurationManager _config = default!;
- [Dependency] private readonly UserInterfaceSystem _bui = default!;
- [Dependency] private readonly IChatManager _chatManager = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly EmagSystem _emag = default!;
-
- public IReadOnlyList SharedMoods => _sharedMoods.AsReadOnly();
- private readonly List _sharedMoods = new();
- // cached hashset that never gets modified
- private readonly HashSet _emptyMoods = new HashSet();
- // cached hashset that gets changed in GetMoodProtoSet
- private readonly HashSet> _moodProtos = new HashSet>();
-
- private ProtoId SharedDataset = "ThavenMoodsShared";
- private ProtoId YesAndDataset = "ThavenMoodsYesAnd";
- private ProtoId NoAndDataset = "ThavenMoodsNoAnd";
- private ProtoId WildcardDataset = "ThavenMoodsWildcard";
-
- private EntProtoId ActionViewMoods = "ActionViewMoods";
-
- private ProtoId RandomThavenMoodDataset = "RandomThavenMoodDataset";
-
- public override void Initialize()
- {
- base.Initialize();
-
- NewSharedMoods();
-
- SubscribeLocalEvent(OnThavenMoodInit);
- SubscribeLocalEvent(OnThavenMoodShutdown);
- SubscribeLocalEvent(OnToggleMoodsScreen);
- SubscribeLocalEvent(OnBoundUIOpened);
- SubscribeLocalEvent(OnIonStorm);
- SubscribeLocalEvent((_) => NewSharedMoods());
- }
-
- public void NewSharedMoods()
- {
- _sharedMoods.Clear();
- for (int i = 0; i < _config.GetCVar(ImpCCVars.ThavenSharedMoodCount); i++)
- TryAddSharedMood(notify: false); // don't spam notify if there are multiple moods
-
- NotifySharedMoodChange();
- }
-
- public bool TryAddSharedMood(ThavenMood? mood = null, bool checkConflicts = true, bool notify = true)
- {
- if (mood == null)
- {
- if (!TryPick(SharedDataset, out var moodProto, _sharedMoods))
- return false;
-
- mood = RollMood(moodProto);
- checkConflicts = false; // TryPick has cleared this mood already
- }
-
- if (checkConflicts && SharedMoodConflicts(mood))
- return false;
-
- _sharedMoods.Add(mood);
-
- if (notify)
- NotifySharedMoodChange();
-
- return true;
- }
-
- private bool SharedMoodConflicts(ThavenMood mood)
- {
- return mood.ProtoId is { } id &&
- (GetConflicts(_sharedMoods).Contains(id) ||
- GetMoodProtoSet(_sharedMoods).Overlaps(mood.Conflicts));
- }
-
- private void NotifySharedMoodChange()
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var comp))
- {
- if (!comp.FollowsSharedMoods)
- continue;
-
- NotifyMoodChange((uid, comp));
- }
- }
-
- private void OnBoundUIOpened(Entity ent, ref BoundUIOpenedEvent args)
- {
- UpdateBUIState(ent);
- }
-
- private void OnToggleMoodsScreen(Entity ent, ref ToggleMoodsScreenEvent args)
- {
- if (args.Handled || !TryComp(ent, out var actor))
- return;
-
- args.Handled = true;
-
- _bui.TryToggleUi(ent.Owner, ThavenMoodsUiKey.Key, actor.PlayerSession);
- }
-
- private bool TryPick(string datasetProto, [NotNullWhen(true)] out ThavenMoodPrototype? proto, IEnumerable? currentMoods = null, HashSet>? conflicts = null)
- {
- var dataset = _proto.Index(datasetProto);
- var choices = dataset.Values.ToList();
-
- currentMoods ??= _emptyMoods;
- conflicts ??= GetConflicts(currentMoods);
-
- var currentMoodProtos = GetMoodProtoSet(currentMoods);
-
- while (choices.Count > 0)
- {
- var moodId = _random.PickAndTake(choices);
- if (conflicts.Contains(moodId))
- continue; // Skip proto if an existing mood conflicts with it
-
- var moodProto = _proto.Index(moodId);
- if (moodProto.Conflicts.Overlaps(currentMoodProtos))
- continue; // Skip proto if it conflicts with an existing mood
-
- proto = moodProto;
- return true;
- }
-
- proto = null;
- return false;
- }
-
- ///
- /// Send the player a audiovisual notification and update the moods UI.
- ///
- public void NotifyMoodChange(Entity ent)
- {
- if (!TryComp(ent, out var actor))
- return;
-
- var session = actor.PlayerSession;
- _audio.PlayGlobal(ent.Comp.MoodsChangedSound, session);
-
- var msg = Loc.GetString("thaven-moods-update-notify");
- var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
- _chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, session.Channel, colorOverride: Color.Orange);
-
- // update the UI without needing to re-open it
- UpdateBUIState(ent);
- }
-
- public void UpdateBUIState(Entity ent)
- {
- var state = new ThavenMoodsBuiState(ent.Comp.FollowsSharedMoods ? _sharedMoods : []);
- _bui.SetUiState(ent.Owner, ThavenMoodsUiKey.Key, state);
- }
-
- ///
- /// Directly add a mood to a thaven, ignoring conflicts.
- ///
- public void AddMood(Entity ent, ThavenMood mood, bool notify = true)
- {
- ent.Comp.Moods.Add(mood);
- Dirty(ent);
-
- if (notify)
- NotifyMoodChange(ent);
- else // NotifyMoodChange will update UI so this is in else
- UpdateBUIState(ent);
- }
-
- ///
- /// Creates a ThavenMood instance from the given ThavenMoodPrototype, and rolls
- /// its mood vars.
- ///
- public ThavenMood RollMood(ThavenMoodPrototype proto)
- {
- var mood = proto.ShallowClone();
- var alreadyChosen = new HashSet>();
-
- foreach (var (name, datasetID) in proto.MoodVarDatasets)
- {
- var dataset = _proto.Index(datasetID);
-
- if (proto.AllowDuplicateMoodVars)
- {
- mood.MoodVars.Add(name, _random.Pick(dataset));
- continue;
- }
-
- var choices = dataset.Values.ToList();
- var foundChoice = false;
- while (choices.Count > 0)
- {
- var choice = _random.PickAndTake(choices);
- if (alreadyChosen.Contains(choice) || mood.MoodVars.ContainsValue(choice))
- continue;
-
- mood.MoodVars.TryAdd(name, choice);
- alreadyChosen.Add(choice);
- foundChoice = true;
- break;
- }
-
- if (!foundChoice)
- {
- //Log.Warning($"Ran out of choices for moodvar \"{name}\" in \"{proto.ID}\"! Picking a duplicate...");
- //mood.MoodVars.Add(name, _random.Pick(dataset));
-
- Log.Warning($"Ran out of choices for moodvar \"{name}\" in \"{proto.ID}\"!"); // You can't add duplicates to dicts, what is the goal here?
- }
- }
-
- return mood;
- }
-
- ///
- /// Checks if the given mood prototype conflicts with the current moods, and
- /// adds the mood if it does not.
- ///
- public bool TryAddMood(Entity ent, ThavenMoodPrototype moodProto, bool allowConflict = false, bool notify = true)
- {
- if (!allowConflict && GetConflicts(ent).Contains(moodProto.ID))
- return false;
-
- AddMood(ent, RollMood(moodProto), notify);
- return true;
- }
-
- ///
- /// Tries to add a random mood using a specific dataset.
- ///
- public bool TryAddRandomMood(Entity ent, string datasetProto, bool notify = true)
- {
- if (TryPick(datasetProto, out var moodProto, GetActiveMoods(ent)))
- {
- AddMood(ent, RollMood(moodProto), notify);
- return true;
- }
-
- return false;
- }
-
- ///
- /// Tries to add a random mood using .
- ///
- public bool TryAddRandomMood(Entity ent, bool notify = true)
- {
- var datasetProto = _proto.Index(RandomThavenMoodDataset).Pick();
- return TryAddRandomMood(ent, datasetProto, notify);
- }
-
- ///
- /// Tries to add a random mood from , which is the same as emagging.
- ///
- public bool AddWildcardMood(Entity ent, bool notify = true)
- {
- return TryAddRandomMood(ent, WildcardDataset, notify);
- }
-
- ///
- /// Set the moods for a thaven directly.
- /// This does NOT check conflicts so be careful with what you set!
- ///
- public void SetMoods(Entity ent, IEnumerable moods, bool notify = true)
- {
- ent.Comp.Moods = moods.ToList();
- Dirty(ent);
-
- if (notify)
- NotifyMoodChange(ent);
- else
- UpdateBUIState(ent);
- }
-
- ///
- /// Clears the moods for a thaven, then applies a new set of moods.
- ///
- public void RefreshMoods(Entity ent)
- {
- ent.Comp.Moods = _emptyMoods.ToList();
-
- // "Yes, and" moods
- if (TryPick(YesAndDataset, out var mood, GetActiveMoods(ent)))
- TryAddMood(ent, mood, true, false);
-
- // "No, and" moods
- if (TryPick(NoAndDataset, out mood, GetActiveMoods(ent)))
- TryAddMood(ent, mood, true, false);
-
- // Wildcard moods
- if (_emag.CheckFlag(ent, EmagType.Interaction))
- AddWildcardMood(ent, false);
- }
-
- public HashSet> GetConflicts(IEnumerable moods)
- {
- var conflicts = new HashSet>();
-
- foreach (var mood in moods)
- {
- if (mood.ProtoId is { } id)
- conflicts.Add(id); // Specific moods shouldn't be added twice
- conflicts.UnionWith(mood.Conflicts);
- }
-
- return conflicts;
- }
-
- ///
- /// Get the conflicts for a thaven's active moods.
- ///
- public HashSet> GetConflicts(Entity ent)
- {
- // TODO: Should probably cache this when moods get updated
- return GetConflicts(GetActiveMoods(ent));
- }
-
- ///
- /// Maps some moods to their ids.
- /// The hashset returned is reused and so you must not modify it.
- ///
- public HashSet> GetMoodProtoSet(IEnumerable moods)
- {
- _moodProtos.Clear();
- foreach (var mood in moods)
- {
- if (mood.ProtoId is { } id)
- _moodProtos.Add(id);
- }
-
- return _moodProtos;
- }
-
- public void SetFollowsSharedmood(Entity