Namespace cleanup around Mind Roles (#30965)
* namespaces * Comment does not need a semicolon --------- Co-authored-by: Vasilis <vascreeper@yahoo.com>
This commit is contained in:
@@ -7,67 +7,66 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Administration
|
||||
namespace Content.Client.Administration;
|
||||
|
||||
internal sealed class AdminNameOverlay : Overlay
|
||||
{
|
||||
internal sealed class AdminNameOverlay : Overlay
|
||||
private readonly AdminSystem _system;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IEyeManager _eyeManager;
|
||||
private readonly EntityLookupSystem _entityLookup;
|
||||
private readonly Font _font;
|
||||
|
||||
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
|
||||
{
|
||||
private readonly AdminSystem _system;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IEyeManager _eyeManager;
|
||||
private readonly EntityLookupSystem _entityLookup;
|
||||
private readonly Font _font;
|
||||
_system = system;
|
||||
_entityManager = entityManager;
|
||||
_eyeManager = eyeManager;
|
||||
_entityLookup = entityLookup;
|
||||
ZIndex = 200;
|
||||
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
}
|
||||
|
||||
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var viewport = args.WorldAABB;
|
||||
|
||||
foreach (var playerInfo in _system.PlayerList)
|
||||
{
|
||||
_system = system;
|
||||
_entityManager = entityManager;
|
||||
_eyeManager = eyeManager;
|
||||
_entityLookup = entityLookup;
|
||||
ZIndex = 200;
|
||||
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
}
|
||||
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var viewport = args.WorldAABB;
|
||||
|
||||
foreach (var playerInfo in _system.PlayerList)
|
||||
// Otherwise the entity can not exist yet
|
||||
if (entity == null || !_entityManager.EntityExists(entity))
|
||||
{
|
||||
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
|
||||
|
||||
// Otherwise the entity can not exist yet
|
||||
if (entity == null || !_entityManager.EntityExists(entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if not on the same map, continue
|
||||
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var aabb = _entityLookup.GetWorldAABB(entity.Value);
|
||||
|
||||
// if not on screen, continue
|
||||
if (!aabb.Intersects(in viewport))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var lineoffset = new Vector2(0f, 11f);
|
||||
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
||||
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
||||
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
||||
if (playerInfo.Antag)
|
||||
{
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
|
||||
}
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if not on the same map, continue
|
||||
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var aabb = _entityLookup.GetWorldAABB(entity.Value);
|
||||
|
||||
// if not on screen, continue
|
||||
if (!aabb.Intersects(in viewport))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var lineoffset = new Vector2(0f, 11f);
|
||||
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
||||
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
||||
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
||||
if (playerInfo.Antag)
|
||||
{
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
|
||||
}
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,220 +10,219 @@ using Robust.Client.UserInterface.XAML;
|
||||
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlayerTab : Control
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlayerTab : Control
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
|
||||
private const string ArrowUp = "↑";
|
||||
private const string ArrowDown = "↓";
|
||||
private readonly Color _altColor = Color.FromHex("#292B38");
|
||||
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
|
||||
private readonly AdminSystem _adminSystem;
|
||||
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
|
||||
|
||||
private Header _headerClicked = Header.Username;
|
||||
private bool _ascending = true;
|
||||
private bool _showDisconnected;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
|
||||
|
||||
public PlayerTab()
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
private const string ArrowUp = "↑";
|
||||
private const string ArrowDown = "↓";
|
||||
private readonly Color _altColor = Color.FromHex("#292B38");
|
||||
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
|
||||
private readonly AdminSystem _adminSystem;
|
||||
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
|
||||
_adminSystem = _entManager.System<AdminSystem>();
|
||||
_adminSystem.PlayerListChanged += RefreshPlayerList;
|
||||
_adminSystem.OverlayEnabled += OverlayEnabled;
|
||||
_adminSystem.OverlayDisabled += OverlayDisabled;
|
||||
|
||||
private Header _headerClicked = Header.Username;
|
||||
private bool _ascending = true;
|
||||
private bool _showDisconnected;
|
||||
OverlayButton.OnPressed += OverlayButtonPressed;
|
||||
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
|
||||
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
|
||||
ListHeader.OnHeaderClicked += HeaderClicked;
|
||||
|
||||
public PlayerTab()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
SearchList.SearchBar = SearchLineEdit;
|
||||
SearchList.GenerateItem += GenerateButton;
|
||||
SearchList.DataFilterCondition += DataFilterCondition;
|
||||
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
|
||||
|
||||
_adminSystem = _entManager.System<AdminSystem>();
|
||||
_adminSystem.PlayerListChanged += RefreshPlayerList;
|
||||
_adminSystem.OverlayEnabled += OverlayEnabled;
|
||||
_adminSystem.OverlayDisabled += OverlayDisabled;
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
|
||||
OverlayButton.OnPressed += OverlayButtonPressed;
|
||||
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
|
||||
|
||||
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
|
||||
ListHeader.OnHeaderClicked += HeaderClicked;
|
||||
|
||||
SearchList.SearchBar = SearchLineEdit;
|
||||
SearchList.GenerateItem += GenerateButton;
|
||||
SearchList.DataFilterCondition += DataFilterCondition;
|
||||
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
|
||||
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
|
||||
}
|
||||
|
||||
#region Antag Overlay
|
||||
|
||||
private void OverlayEnabled()
|
||||
{
|
||||
OverlayButton.Pressed = true;
|
||||
}
|
||||
|
||||
private void OverlayDisabled()
|
||||
{
|
||||
OverlayButton.Pressed = false;
|
||||
}
|
||||
|
||||
private void OverlayButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button.Pressed)
|
||||
{
|
||||
_adminSystem.AdminOverlayOn();
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminSystem.AdminOverlayOff();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ShowDisconnectedPressed(ButtonEventArgs args)
|
||||
{
|
||||
_showDisconnected = args.Button.Pressed;
|
||||
RefreshPlayerList(_players);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_adminSystem.PlayerListChanged -= RefreshPlayerList;
|
||||
_adminSystem.OverlayEnabled -= OverlayEnabled;
|
||||
_adminSystem.OverlayDisabled -= OverlayDisabled;
|
||||
|
||||
OverlayButton.OnPressed -= OverlayButtonPressed;
|
||||
|
||||
ListHeader.OnHeaderClicked -= HeaderClicked;
|
||||
}
|
||||
}
|
||||
|
||||
#region ListContainer
|
||||
|
||||
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
|
||||
{
|
||||
_players = players;
|
||||
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
|
||||
|
||||
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
|
||||
|
||||
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
|
||||
sortedPlayers.Sort(Compare);
|
||||
|
||||
UpdateHeaderSymbols();
|
||||
|
||||
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
||||
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not PlayerListData { Info: var player})
|
||||
return;
|
||||
|
||||
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
|
||||
button.AddChild(entry);
|
||||
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
|
||||
/// If all characters are lowercase, the comparison ignores case.
|
||||
/// If there is an uppercase character, the comparison is case sensitive.
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <param name="listData"></param>
|
||||
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
|
||||
private bool DataFilterCondition(string filter, ListData listData)
|
||||
{
|
||||
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
|
||||
return false;
|
||||
|
||||
if (!_showDisconnected && !info.Connected)
|
||||
return false;
|
||||
|
||||
if (IsAllLower(filter))
|
||||
{
|
||||
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!playerString.Contains(filter))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsAllLower(string input)
|
||||
{
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (char.IsLetter(c) && !char.IsLower(c))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header
|
||||
|
||||
private void UpdateHeaderSymbols()
|
||||
{
|
||||
ListHeader.ResetHeaderText();
|
||||
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
|
||||
}
|
||||
|
||||
private int Compare(PlayerInfo x, PlayerInfo y)
|
||||
{
|
||||
if (!_ascending)
|
||||
{
|
||||
(x, y) = (y, x);
|
||||
}
|
||||
|
||||
return _headerClicked switch
|
||||
{
|
||||
Header.Username => Compare(x.Username, y.Username),
|
||||
Header.Character => Compare(x.CharacterName, y.CharacterName),
|
||||
Header.Job => Compare(x.StartingJob, y.StartingJob),
|
||||
Header.Antagonist => x.Antag.CompareTo(y.Antag),
|
||||
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
|
||||
_ => 1
|
||||
};
|
||||
}
|
||||
|
||||
private int Compare(string x, string y)
|
||||
{
|
||||
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void HeaderClicked(Header header)
|
||||
{
|
||||
if (_headerClicked == header)
|
||||
{
|
||||
_ascending = !_ascending;
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerClicked = header;
|
||||
_ascending = true;
|
||||
}
|
||||
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
|
||||
#region Antag Overlay
|
||||
|
||||
private void OverlayEnabled()
|
||||
{
|
||||
OverlayButton.Pressed = true;
|
||||
}
|
||||
|
||||
private void OverlayDisabled()
|
||||
{
|
||||
OverlayButton.Pressed = false;
|
||||
}
|
||||
|
||||
private void OverlayButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button.Pressed)
|
||||
{
|
||||
_adminSystem.AdminOverlayOn();
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminSystem.AdminOverlayOff();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ShowDisconnectedPressed(ButtonEventArgs args)
|
||||
{
|
||||
_showDisconnected = args.Button.Pressed;
|
||||
RefreshPlayerList(_players);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_adminSystem.PlayerListChanged -= RefreshPlayerList;
|
||||
_adminSystem.OverlayEnabled -= OverlayEnabled;
|
||||
_adminSystem.OverlayDisabled -= OverlayDisabled;
|
||||
|
||||
OverlayButton.OnPressed -= OverlayButtonPressed;
|
||||
|
||||
ListHeader.OnHeaderClicked -= HeaderClicked;
|
||||
}
|
||||
}
|
||||
|
||||
#region ListContainer
|
||||
|
||||
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
|
||||
{
|
||||
_players = players;
|
||||
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
|
||||
|
||||
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
|
||||
|
||||
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
|
||||
sortedPlayers.Sort(Compare);
|
||||
|
||||
UpdateHeaderSymbols();
|
||||
|
||||
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
||||
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not PlayerListData { Info: var player})
|
||||
return;
|
||||
|
||||
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
|
||||
button.AddChild(entry);
|
||||
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
|
||||
/// If all characters are lowercase, the comparison ignores case.
|
||||
/// If there is an uppercase character, the comparison is case sensitive.
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <param name="listData"></param>
|
||||
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
|
||||
private bool DataFilterCondition(string filter, ListData listData)
|
||||
{
|
||||
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
|
||||
return false;
|
||||
|
||||
if (!_showDisconnected && !info.Connected)
|
||||
return false;
|
||||
|
||||
if (IsAllLower(filter))
|
||||
{
|
||||
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!playerString.Contains(filter))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsAllLower(string input)
|
||||
{
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (char.IsLetter(c) && !char.IsLower(c))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header
|
||||
|
||||
private void UpdateHeaderSymbols()
|
||||
{
|
||||
ListHeader.ResetHeaderText();
|
||||
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
|
||||
}
|
||||
|
||||
private int Compare(PlayerInfo x, PlayerInfo y)
|
||||
{
|
||||
if (!_ascending)
|
||||
{
|
||||
(x, y) = (y, x);
|
||||
}
|
||||
|
||||
return _headerClicked switch
|
||||
{
|
||||
Header.Username => Compare(x.Username, y.Username),
|
||||
Header.Character => Compare(x.CharacterName, y.CharacterName),
|
||||
Header.Job => Compare(x.StartingJob, y.StartingJob),
|
||||
Header.Antagonist => x.Antag.CompareTo(y.Antag),
|
||||
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
|
||||
_ => 1
|
||||
};
|
||||
}
|
||||
|
||||
private int Compare(string x, string y)
|
||||
{
|
||||
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void HeaderClicked(Header header)
|
||||
{
|
||||
if (_headerClicked == header)
|
||||
{
|
||||
_ascending = !_ascending;
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerClicked = header;
|
||||
_ascending = true;
|
||||
}
|
||||
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
|
||||
|
||||
@@ -5,71 +5,70 @@ using Content.Shared.Chat;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Chat.Managers
|
||||
namespace Content.Client.Chat.Managers;
|
||||
|
||||
internal sealed class ChatManager : IChatManager
|
||||
{
|
||||
internal sealed class ChatManager : IChatManager
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
_sawmill = Logger.GetSawmill("chat");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
}
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public void Initialize()
|
||||
public void SendMessage(string text, ChatSelectChannel channel)
|
||||
{
|
||||
var str = text.ToString();
|
||||
switch (channel)
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("chat");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
}
|
||||
case ChatSelectChannel.Console:
|
||||
// run locally
|
||||
_consoleHost.ExecuteCommand(text);
|
||||
break;
|
||||
|
||||
public void SendMessage(string text, ChatSelectChannel channel)
|
||||
{
|
||||
var str = text.ToString();
|
||||
switch (channel)
|
||||
{
|
||||
case ChatSelectChannel.Console:
|
||||
// run locally
|
||||
_consoleHost.ExecuteCommand(text);
|
||||
break;
|
||||
case ChatSelectChannel.LOOC:
|
||||
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.LOOC:
|
||||
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
case ChatSelectChannel.OOC:
|
||||
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.OOC:
|
||||
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
case ChatSelectChannel.Admin:
|
||||
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.Admin:
|
||||
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
case ChatSelectChannel.Emotes:
|
||||
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.Emotes:
|
||||
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
case ChatSelectChannel.Dead:
|
||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
||||
goto case ChatSelectChannel.Local;
|
||||
|
||||
case ChatSelectChannel.Dead:
|
||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
||||
goto case ChatSelectChannel.Local;
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
|
||||
else
|
||||
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
|
||||
break;
|
||||
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
|
||||
else
|
||||
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
|
||||
break;
|
||||
// TODO sepearate radio and say into separate commands.
|
||||
case ChatSelectChannel.Radio:
|
||||
case ChatSelectChannel.Local:
|
||||
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
// TODO sepearate radio and say into separate commands.
|
||||
case ChatSelectChannel.Radio:
|
||||
case ChatSelectChannel.Local:
|
||||
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
case ChatSelectChannel.Whisper:
|
||||
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.Whisper:
|
||||
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,426 +31,425 @@ using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Administration.Systems
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
public sealed class AdminSystem : EntitySystem
|
||||
{
|
||||
public sealed class AdminSystem : EntitySystem
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MindSystem _minds = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of players that have participated in this round.
|
||||
/// </summary>
|
||||
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
||||
|
||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||
public readonly PanicBunkerStatus PanicBunker = new();
|
||||
public readonly BabyJailStatus BabyJail = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MindSystem _minds = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
base.Initialize();
|
||||
|
||||
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_adminManager.OnPermsChanged += OnAdminPermsChanged;
|
||||
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Set of players that have participated in this round.
|
||||
/// </summary>
|
||||
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
||||
// Panic Bunker Settings
|
||||
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
||||
|
||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||
public readonly PanicBunkerStatus PanicBunker = new();
|
||||
public readonly BabyJailStatus BabyJail = new();
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
public override void Initialize()
|
||||
// Baby Jail Settings
|
||||
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
||||
|
||||
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||
}
|
||||
|
||||
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_roundActivePlayers.Clear();
|
||||
|
||||
foreach (var (id, data) in _playerList)
|
||||
{
|
||||
base.Initialize();
|
||||
if (!data.ActiveThisRound)
|
||||
continue;
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_adminManager.OnPermsChanged += OnAdminPermsChanged;
|
||||
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
|
||||
|
||||
// Panic Bunker Settings
|
||||
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
// Baby Jail Settings
|
||||
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
||||
|
||||
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||
}
|
||||
|
||||
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_roundActivePlayers.Clear();
|
||||
|
||||
foreach (var (id, data) in _playerList)
|
||||
{
|
||||
if (!data.ActiveThisRound)
|
||||
continue;
|
||||
|
||||
if (!_playerManager.TryGetPlayerData(id, out var playerData))
|
||||
return;
|
||||
|
||||
_playerManager.TryGetSessionById(id, out var session);
|
||||
_playerList[id] = GetPlayerInfo(playerData, session);
|
||||
}
|
||||
|
||||
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
|
||||
|
||||
foreach (var admin in _adminManager.ActiveAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(updateEv, admin.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePlayerList(ICommonSession player)
|
||||
{
|
||||
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
|
||||
|
||||
var playerInfoChangedEvent = new PlayerInfoChangedEvent
|
||||
{
|
||||
PlayerInfo = _playerList[player.UserId]
|
||||
};
|
||||
|
||||
foreach (var admin in _adminManager.ActiveAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
|
||||
{
|
||||
if (netUserId == null)
|
||||
return null;
|
||||
|
||||
_playerList.TryGetValue(netUserId.Value, out var value);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
private void OnIdentityChanged(ref IdentityChangedEvent ev)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
|
||||
if (!_playerManager.TryGetPlayerData(id, out var playerData))
|
||||
return;
|
||||
|
||||
UpdatePlayerList(actor.PlayerSession);
|
||||
_playerManager.TryGetSessionById(id, out var session);
|
||||
_playerList[id] = GetPlayerInfo(playerData, session);
|
||||
}
|
||||
|
||||
private void OnRoleEvent(RoleEvent ev)
|
||||
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
|
||||
|
||||
foreach (var admin in _adminManager.ActiveAdmins)
|
||||
{
|
||||
var session = _minds.GetSession(ev.Mind);
|
||||
if (!ev.Antagonist || session == null)
|
||||
return;
|
||||
|
||||
UpdatePlayerList(session);
|
||||
}
|
||||
|
||||
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
|
||||
{
|
||||
UpdatePanicBunker();
|
||||
|
||||
if (!obj.IsAdmin)
|
||||
{
|
||||
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
|
||||
return;
|
||||
}
|
||||
|
||||
SendFullPlayerList(obj.Player);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(PlayerDetachedEvent ev)
|
||||
{
|
||||
// If disconnected then the player won't have a connected entity to get character name from.
|
||||
// The disconnected state gets sent by OnPlayerStatusChanged.
|
||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
UpdatePlayerList(ev.Player);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
||||
{
|
||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
_roundActivePlayers.Add(ev.Player.UserId);
|
||||
UpdatePlayerList(ev.Player);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
|
||||
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
UpdatePlayerList(e.Session);
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void SendFullPlayerList(ICommonSession playerSession)
|
||||
{
|
||||
var ev = new FullPlayerListEvent();
|
||||
|
||||
ev.PlayersInfo = _playerList.Values.ToList();
|
||||
|
||||
RaiseNetworkEvent(ev, playerSession.Channel);
|
||||
}
|
||||
|
||||
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
|
||||
{
|
||||
var name = data.UserName;
|
||||
var entityName = string.Empty;
|
||||
var identityName = string.Empty;
|
||||
|
||||
if (session?.AttachedEntity != null)
|
||||
{
|
||||
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
||||
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
|
||||
}
|
||||
|
||||
var antag = false;
|
||||
var startingRole = string.Empty;
|
||||
if (_minds.TryGetMind(session, out var mindId, out _))
|
||||
{
|
||||
antag = _role.MindIsAntagonist(mindId);
|
||||
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||
}
|
||||
|
||||
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
||||
TimeSpan? overallPlaytime = null;
|
||||
if (session != null &&
|
||||
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
||||
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
||||
{
|
||||
overallPlaytime = playTime;
|
||||
}
|
||||
|
||||
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
|
||||
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
|
||||
}
|
||||
|
||||
private void OnPanicBunkerChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-panic-bunker-enabled-admin-alert"
|
||||
: "admin-ui-panic-bunker-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailChanged(bool enabled)
|
||||
{
|
||||
BabyJail.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-baby-jail-enabled-admin-alert"
|
||||
: "admin-ui-baby-jail-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.DisableWithAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.EnableWithoutAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.CountDeadminnedAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerShowReasonChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.ShowReason = enabled;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailShowReasonChanged(bool enabled)
|
||||
{
|
||||
BabyJail.ShowReason = enabled;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinAccountAgeMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxAccountAgeMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinOverallMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxOverallMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void UpdatePanicBunker()
|
||||
{
|
||||
var admins = PanicBunker.CountDeadminnedAdmins
|
||||
? _adminManager.AllAdmins
|
||||
: _adminManager.ActiveAdmins;
|
||||
var hasAdmins = admins.Any();
|
||||
|
||||
// TODO Fix order dependent Cvars
|
||||
// Please for the sake of my sanity don't make cvars & order dependent.
|
||||
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
|
||||
//
|
||||
// I.e., this:
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// and this:
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
//
|
||||
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
|
||||
|
||||
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
||||
{
|
||||
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
||||
}
|
||||
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
|
||||
{
|
||||
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
||||
}
|
||||
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void SendPanicBunkerStatusAll()
|
||||
{
|
||||
var ev = new PanicBunkerChangedEvent(PanicBunker);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBabyJailStatusAll()
|
||||
{
|
||||
var ev = new BabyJailChangedEvent(BabyJail);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erases a player from the round.
|
||||
/// This removes them and any trace of them from the round, deleting their
|
||||
/// chat messages and showing a popup to other players.
|
||||
/// Their items are dropped on the ground.
|
||||
/// </summary>
|
||||
public void Erase(ICommonSession player)
|
||||
{
|
||||
var entity = player.AttachedEntity;
|
||||
_chat.DeleteMessagesBy(player);
|
||||
|
||||
if (entity != null && !TerminatingOrDeleted(entity.Value))
|
||||
{
|
||||
if (TryComp(entity.Value, out TransformComponent? transform))
|
||||
{
|
||||
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
|
||||
var name = Identity.Entity(entity.Value, EntityManager);
|
||||
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
|
||||
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
|
||||
var audioParams = new AudioParams().WithVolume(3);
|
||||
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
|
||||
{
|
||||
if (TryComp(item, out PdaComponent? pda) &&
|
||||
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
|
||||
keyStorage.Key is { } key &&
|
||||
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
|
||||
{
|
||||
if (TryComp(entity, out DnaComponent? dna) &&
|
||||
dna.DNA != record.DNA)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
|
||||
fingerPrint.Fingerprint != record.Fingerprint)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_stationRecords.RemoveRecord(key);
|
||||
Del(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
|
||||
{
|
||||
while (enumerator.NextItem(out var item, out var slot))
|
||||
{
|
||||
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
|
||||
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp(entity.Value, out HandsComponent? hands))
|
||||
{
|
||||
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
|
||||
{
|
||||
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_minds.WipeMind(player);
|
||||
QueueDel(entity);
|
||||
|
||||
_gameTicker.SpawnObserver(player);
|
||||
}
|
||||
|
||||
private void OnSessionPlayTimeUpdated(ICommonSession session)
|
||||
{
|
||||
UpdatePlayerList(session);
|
||||
RaiseNetworkEvent(updateEv, admin.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePlayerList(ICommonSession player)
|
||||
{
|
||||
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
|
||||
|
||||
var playerInfoChangedEvent = new PlayerInfoChangedEvent
|
||||
{
|
||||
PlayerInfo = _playerList[player.UserId]
|
||||
};
|
||||
|
||||
foreach (var admin in _adminManager.ActiveAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
|
||||
{
|
||||
if (netUserId == null)
|
||||
return null;
|
||||
|
||||
_playerList.TryGetValue(netUserId.Value, out var value);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
private void OnIdentityChanged(ref IdentityChangedEvent ev)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
|
||||
return;
|
||||
|
||||
UpdatePlayerList(actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void OnRoleEvent(RoleEvent ev)
|
||||
{
|
||||
var session = _minds.GetSession(ev.Mind);
|
||||
if (!ev.Antagonist || session == null)
|
||||
return;
|
||||
|
||||
UpdatePlayerList(session);
|
||||
}
|
||||
|
||||
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
|
||||
{
|
||||
UpdatePanicBunker();
|
||||
|
||||
if (!obj.IsAdmin)
|
||||
{
|
||||
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
|
||||
return;
|
||||
}
|
||||
|
||||
SendFullPlayerList(obj.Player);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(PlayerDetachedEvent ev)
|
||||
{
|
||||
// If disconnected then the player won't have a connected entity to get character name from.
|
||||
// The disconnected state gets sent by OnPlayerStatusChanged.
|
||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
UpdatePlayerList(ev.Player);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
||||
{
|
||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
_roundActivePlayers.Add(ev.Player.UserId);
|
||||
UpdatePlayerList(ev.Player);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
|
||||
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
UpdatePlayerList(e.Session);
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void SendFullPlayerList(ICommonSession playerSession)
|
||||
{
|
||||
var ev = new FullPlayerListEvent();
|
||||
|
||||
ev.PlayersInfo = _playerList.Values.ToList();
|
||||
|
||||
RaiseNetworkEvent(ev, playerSession.Channel);
|
||||
}
|
||||
|
||||
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
|
||||
{
|
||||
var name = data.UserName;
|
||||
var entityName = string.Empty;
|
||||
var identityName = string.Empty;
|
||||
|
||||
if (session?.AttachedEntity != null)
|
||||
{
|
||||
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
||||
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
|
||||
}
|
||||
|
||||
var antag = false;
|
||||
var startingRole = string.Empty;
|
||||
if (_minds.TryGetMind(session, out var mindId, out _))
|
||||
{
|
||||
antag = _role.MindIsAntagonist(mindId);
|
||||
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||
}
|
||||
|
||||
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
||||
TimeSpan? overallPlaytime = null;
|
||||
if (session != null &&
|
||||
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
||||
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
||||
{
|
||||
overallPlaytime = playTime;
|
||||
}
|
||||
|
||||
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
|
||||
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
|
||||
}
|
||||
|
||||
private void OnPanicBunkerChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-panic-bunker-enabled-admin-alert"
|
||||
: "admin-ui-panic-bunker-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailChanged(bool enabled)
|
||||
{
|
||||
BabyJail.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-baby-jail-enabled-admin-alert"
|
||||
: "admin-ui-baby-jail-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.DisableWithAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.EnableWithoutAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.CountDeadminnedAdmins = enabled;
|
||||
UpdatePanicBunker();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerShowReasonChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.ShowReason = enabled;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailShowReasonChanged(bool enabled)
|
||||
{
|
||||
BabyJail.ShowReason = enabled;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinAccountAgeMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxAccountAgeMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinOverallMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxOverallMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void UpdatePanicBunker()
|
||||
{
|
||||
var admins = PanicBunker.CountDeadminnedAdmins
|
||||
? _adminManager.AllAdmins
|
||||
: _adminManager.ActiveAdmins;
|
||||
var hasAdmins = admins.Any();
|
||||
|
||||
// TODO Fix order dependent Cvars
|
||||
// Please for the sake of my sanity don't make cvars & order dependent.
|
||||
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
|
||||
//
|
||||
// I.e., this:
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// and this:
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
//
|
||||
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
|
||||
|
||||
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
||||
{
|
||||
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
||||
}
|
||||
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
|
||||
{
|
||||
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
||||
}
|
||||
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void SendPanicBunkerStatusAll()
|
||||
{
|
||||
var ev = new PanicBunkerChangedEvent(PanicBunker);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBabyJailStatusAll()
|
||||
{
|
||||
var ev = new BabyJailChangedEvent(BabyJail);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erases a player from the round.
|
||||
/// This removes them and any trace of them from the round, deleting their
|
||||
/// chat messages and showing a popup to other players.
|
||||
/// Their items are dropped on the ground.
|
||||
/// </summary>
|
||||
public void Erase(ICommonSession player)
|
||||
{
|
||||
var entity = player.AttachedEntity;
|
||||
_chat.DeleteMessagesBy(player);
|
||||
|
||||
if (entity != null && !TerminatingOrDeleted(entity.Value))
|
||||
{
|
||||
if (TryComp(entity.Value, out TransformComponent? transform))
|
||||
{
|
||||
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
|
||||
var name = Identity.Entity(entity.Value, EntityManager);
|
||||
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
|
||||
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
|
||||
var audioParams = new AudioParams().WithVolume(3);
|
||||
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
|
||||
{
|
||||
if (TryComp(item, out PdaComponent? pda) &&
|
||||
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
|
||||
keyStorage.Key is { } key &&
|
||||
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
|
||||
{
|
||||
if (TryComp(entity, out DnaComponent? dna) &&
|
||||
dna.DNA != record.DNA)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
|
||||
fingerPrint.Fingerprint != record.Fingerprint)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_stationRecords.RemoveRecord(key);
|
||||
Del(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
|
||||
{
|
||||
while (enumerator.NextItem(out var item, out var slot))
|
||||
{
|
||||
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
|
||||
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp(entity.Value, out HandsComponent? hands))
|
||||
{
|
||||
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
|
||||
{
|
||||
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_minds.WipeMind(player);
|
||||
QueueDel(entity);
|
||||
|
||||
_gameTicker.SpawnObserver(player);
|
||||
}
|
||||
|
||||
private void OnSessionPlayTimeUpdated(ICommonSession session)
|
||||
{
|
||||
UpdatePlayerList(session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,385 +18,384 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Chat.Managers
|
||||
namespace Content.Server.Chat.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches chat messages to clients.
|
||||
/// </summary>
|
||||
internal sealed partial class ChatManager : IChatManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches chat messages to clients.
|
||||
/// </summary>
|
||||
internal sealed partial class ChatManager : IChatManager
|
||||
private static readonly Dictionary<string, string> PatronOocColors = new()
|
||||
{
|
||||
private static readonly Dictionary<string, string> PatronOocColors = new()
|
||||
// I had plans for multiple colors and those went nowhere so...
|
||||
{ "nuclear_operative", "#aa00ff" },
|
||||
{ "syndicate_agent", "#aa00ff" },
|
||||
{ "revolutionary", "#aa00ff" }
|
||||
};
|
||||
|
||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMoMMILink _mommiLink = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length a player-sent message can be sent
|
||||
/// </summary>
|
||||
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
|
||||
|
||||
private bool _oocEnabled = true;
|
||||
private bool _adminOocEnabled = true;
|
||||
|
||||
private readonly Dictionary<NetUserId, ChatUser> _players = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgChatMessage>();
|
||||
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
||||
|
||||
RegisterRateLimits();
|
||||
}
|
||||
|
||||
private void OnOocEnabledChanged(bool val)
|
||||
{
|
||||
if (_oocEnabled == val) return;
|
||||
|
||||
_oocEnabled = val;
|
||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
|
||||
}
|
||||
|
||||
private void OnAdminOocEnabledChanged(bool val)
|
||||
{
|
||||
if (_adminOocEnabled == val) return;
|
||||
|
||||
_adminOocEnabled = val;
|
||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
|
||||
}
|
||||
|
||||
public void DeleteMessagesBy(ICommonSession player)
|
||||
{
|
||||
if (!_players.TryGetValue(player.UserId, out var user))
|
||||
return;
|
||||
|
||||
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
|
||||
_netManager.ServerSendToAll(msg);
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull(nameof(author))]
|
||||
public ChatUser? EnsurePlayer(NetUserId? author)
|
||||
{
|
||||
if (author == null)
|
||||
return null;
|
||||
|
||||
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
|
||||
if (!exists || user == null)
|
||||
user = new ChatUser(_players.Count);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
#region Server Announcements
|
||||
|
||||
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
|
||||
Logger.InfoS("SERVER", message);
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
|
||||
}
|
||||
|
||||
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
||||
|
||||
if (!suppressLog)
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
|
||||
}
|
||||
|
||||
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
|
||||
{
|
||||
var clients = _adminManager.ActiveAdmins.Where(p =>
|
||||
{
|
||||
// I had plans for multiple colors and those went nowhere so...
|
||||
{ "nuclear_operative", "#aa00ff" },
|
||||
{ "syndicate_agent", "#aa00ff" },
|
||||
{ "revolutionary", "#aa00ff" }
|
||||
};
|
||||
var adminData = _adminManager.GetAdminData(p);
|
||||
|
||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMoMMILink _mommiLink = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
||||
DebugTools.AssertNotNull(adminData);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length a player-sent message can be sent
|
||||
/// </summary>
|
||||
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
|
||||
|
||||
private bool _oocEnabled = true;
|
||||
private bool _adminOocEnabled = true;
|
||||
|
||||
private readonly Dictionary<NetUserId, ChatUser> _players = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgChatMessage>();
|
||||
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
||||
|
||||
RegisterRateLimits();
|
||||
}
|
||||
|
||||
private void OnOocEnabledChanged(bool val)
|
||||
{
|
||||
if (_oocEnabled == val) return;
|
||||
|
||||
_oocEnabled = val;
|
||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
|
||||
}
|
||||
|
||||
private void OnAdminOocEnabledChanged(bool val)
|
||||
{
|
||||
if (_adminOocEnabled == val) return;
|
||||
|
||||
_adminOocEnabled = val;
|
||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
|
||||
}
|
||||
|
||||
public void DeleteMessagesBy(ICommonSession player)
|
||||
{
|
||||
if (!_players.TryGetValue(player.UserId, out var user))
|
||||
return;
|
||||
|
||||
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
|
||||
_netManager.ServerSendToAll(msg);
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull(nameof(author))]
|
||||
public ChatUser? EnsurePlayer(NetUserId? author)
|
||||
{
|
||||
if (author == null)
|
||||
return null;
|
||||
|
||||
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
|
||||
if (!exists || user == null)
|
||||
user = new ChatUser(_players.Count);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
#region Server Announcements
|
||||
|
||||
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
|
||||
Logger.InfoS("SERVER", message);
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
|
||||
}
|
||||
|
||||
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
||||
|
||||
if (!suppressLog)
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
|
||||
}
|
||||
|
||||
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
|
||||
{
|
||||
var clients = _adminManager.ActiveAdmins.Where(p =>
|
||||
{
|
||||
var adminData = _adminManager.GetAdminData(p);
|
||||
|
||||
DebugTools.AssertNotNull(adminData);
|
||||
|
||||
if (adminData == null)
|
||||
return false;
|
||||
|
||||
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
|
||||
return false;
|
||||
|
||||
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
|
||||
|
||||
}).Select(p => p.Channel);
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
|
||||
}
|
||||
|
||||
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
|
||||
}
|
||||
|
||||
public void SendAdminAlert(string message)
|
||||
{
|
||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
|
||||
}
|
||||
|
||||
public void SendAdminAlert(EntityUid player, string message)
|
||||
{
|
||||
var mindSystem = _entityManager.System<SharedMindSystem>();
|
||||
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
||||
{
|
||||
SendAdminAlert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
var adminSystem = _entityManager.System<AdminSystem>();
|
||||
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
|
||||
|
||||
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
|
||||
}
|
||||
|
||||
public void SendHookOOC(string sender, string message)
|
||||
{
|
||||
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public OOC Chat API
|
||||
|
||||
/// <summary>
|
||||
/// Called for a player to attempt sending an OOC, out-of-game. message.
|
||||
/// </summary>
|
||||
/// <param name="player">The player sending the message.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="type">The type of message.</param>
|
||||
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
||||
{
|
||||
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// Check if message exceeds the character limit
|
||||
if (message.Length > MaxMessageLength)
|
||||
{
|
||||
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case OOCChatType.OOC:
|
||||
SendOOC(player, message);
|
||||
break;
|
||||
case OOCChatType.Admin:
|
||||
SendAdminChat(player, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private API
|
||||
|
||||
private void SendOOC(ICommonSession player, string message)
|
||||
{
|
||||
if (_adminManager.IsAdmin(player))
|
||||
{
|
||||
if (!_adminOocEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!_oocEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color? colorOverride = null;
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
|
||||
{
|
||||
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
||||
colorOverride = prefs.AdminOOCColor;
|
||||
}
|
||||
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
|
||||
{
|
||||
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
}
|
||||
|
||||
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
|
||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
|
||||
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
private void SendAdminChat(ICommonSession player, string message)
|
||||
{
|
||||
if (!_adminManager.IsAdmin(player))
|
||||
{
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
|
||||
return;
|
||||
}
|
||||
|
||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
var isSource = client != player.Channel;
|
||||
ChatMessageToOne(ChatChannel.AdminChat,
|
||||
message,
|
||||
wrappedMessage,
|
||||
default,
|
||||
false,
|
||||
client,
|
||||
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
|
||||
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
|
||||
author: player.UserId);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility
|
||||
|
||||
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
|
||||
|
||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
|
||||
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
|
||||
{
|
||||
if (!recordReplay && !filter.Recipients.Any())
|
||||
return;
|
||||
|
||||
var clients = new List<INetChannel>();
|
||||
foreach (var recipient in filter.Recipients)
|
||||
{
|
||||
clients.Add(recipient.Channel);
|
||||
}
|
||||
|
||||
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
|
||||
}
|
||||
|
||||
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public bool MessageCharacterLimit(ICommonSession? player, string message)
|
||||
{
|
||||
var isOverLength = false;
|
||||
|
||||
// Non-players don't need to be checked.
|
||||
if (player == null)
|
||||
if (adminData == null)
|
||||
return false;
|
||||
|
||||
// Check if message exceeds the character limit if the sender is a player
|
||||
if (message.Length > MaxMessageLength)
|
||||
{
|
||||
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
|
||||
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
|
||||
return false;
|
||||
|
||||
DispatchServerMessage(player, feedback);
|
||||
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
|
||||
|
||||
isOverLength = true;
|
||||
}
|
||||
}).Select(p => p.Channel);
|
||||
|
||||
return isOverLength;
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
|
||||
}
|
||||
|
||||
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
|
||||
}
|
||||
|
||||
public void SendAdminAlert(string message)
|
||||
{
|
||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
|
||||
}
|
||||
|
||||
public void SendAdminAlert(EntityUid player, string message)
|
||||
{
|
||||
var mindSystem = _entityManager.System<SharedMindSystem>();
|
||||
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
||||
{
|
||||
SendAdminAlert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
#endregion
|
||||
var adminSystem = _entityManager.System<AdminSystem>();
|
||||
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
|
||||
|
||||
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
|
||||
}
|
||||
|
||||
public enum OOCChatType : byte
|
||||
public void SendHookOOC(string sender, string message)
|
||||
{
|
||||
OOC,
|
||||
Admin
|
||||
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public OOC Chat API
|
||||
|
||||
/// <summary>
|
||||
/// Called for a player to attempt sending an OOC, out-of-game. message.
|
||||
/// </summary>
|
||||
/// <param name="player">The player sending the message.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="type">The type of message.</param>
|
||||
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
||||
{
|
||||
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// Check if message exceeds the character limit
|
||||
if (message.Length > MaxMessageLength)
|
||||
{
|
||||
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case OOCChatType.OOC:
|
||||
SendOOC(player, message);
|
||||
break;
|
||||
case OOCChatType.Admin:
|
||||
SendAdminChat(player, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private API
|
||||
|
||||
private void SendOOC(ICommonSession player, string message)
|
||||
{
|
||||
if (_adminManager.IsAdmin(player))
|
||||
{
|
||||
if (!_adminOocEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!_oocEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color? colorOverride = null;
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
|
||||
{
|
||||
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
||||
colorOverride = prefs.AdminOOCColor;
|
||||
}
|
||||
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
|
||||
{
|
||||
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
}
|
||||
|
||||
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
|
||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
|
||||
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
private void SendAdminChat(ICommonSession player, string message)
|
||||
{
|
||||
if (!_adminManager.IsAdmin(player))
|
||||
{
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
|
||||
return;
|
||||
}
|
||||
|
||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
|
||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
var isSource = client != player.Channel;
|
||||
ChatMessageToOne(ChatChannel.AdminChat,
|
||||
message,
|
||||
wrappedMessage,
|
||||
default,
|
||||
false,
|
||||
client,
|
||||
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
|
||||
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
|
||||
author: player.UserId);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility
|
||||
|
||||
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
|
||||
|
||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
|
||||
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
|
||||
{
|
||||
if (!recordReplay && !filter.Recipients.Any())
|
||||
return;
|
||||
|
||||
var clients = new List<INetChannel>();
|
||||
foreach (var recipient in filter.Recipients)
|
||||
{
|
||||
clients.Add(recipient.Channel);
|
||||
}
|
||||
|
||||
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
|
||||
}
|
||||
|
||||
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||
{
|
||||
var user = author == null ? null : EnsurePlayer(author);
|
||||
var netSource = _entityManager.GetNetEntity(source);
|
||||
user?.AddEntity(netSource);
|
||||
|
||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
|
||||
|
||||
if (!recordReplay)
|
||||
return;
|
||||
|
||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||
{
|
||||
_replay.RecordServerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public bool MessageCharacterLimit(ICommonSession? player, string message)
|
||||
{
|
||||
var isOverLength = false;
|
||||
|
||||
// Non-players don't need to be checked.
|
||||
if (player == null)
|
||||
return false;
|
||||
|
||||
// Check if message exceeds the character limit if the sender is a player
|
||||
if (message.Length > MaxMessageLength)
|
||||
{
|
||||
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
|
||||
|
||||
DispatchServerMessage(player, feedback);
|
||||
|
||||
isOverLength = true;
|
||||
}
|
||||
|
||||
return isOverLength;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum OOCChatType : byte
|
||||
{
|
||||
OOC,
|
||||
Admin
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
namespace Content.Server.Ghost
|
||||
namespace Content.Server.Ghost;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to mark Observers properly, as they get Minds
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ObserverRoleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used to mark Observers properly, as they get Minds
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ObserverRoleComponent : Component
|
||||
{
|
||||
public string Name => Loc.GetString("observer-role-name");
|
||||
}
|
||||
public string Name => Loc.GetString("observer-role-name");
|
||||
}
|
||||
|
||||
@@ -2,102 +2,101 @@
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Components
|
||||
namespace Content.Server.Ghost.Roles.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[Access(typeof(GhostRoleSystem))]
|
||||
public sealed partial class GhostRoleComponent : Component
|
||||
{
|
||||
[RegisterComponent]
|
||||
[Access(typeof(GhostRoleSystem))]
|
||||
public sealed partial class GhostRoleComponent : Component
|
||||
[DataField("name")] private string _roleName = "Unknown";
|
||||
|
||||
[DataField("description")] private string _roleDescription = "Unknown";
|
||||
|
||||
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
|
||||
|
||||
// TODO ROLE TIMERS
|
||||
// Actually make use of / enforce this requirement?
|
||||
// Why is this even here.
|
||||
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
||||
[DataField("requirements")]
|
||||
public HashSet<JobRequirement>? Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
|
||||
public bool MakeSentient = true;
|
||||
|
||||
/// <summary>
|
||||
/// The probability that this ghost role will be available after init.
|
||||
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
|
||||
/// </summary>
|
||||
[DataField("prob")]
|
||||
public float Probability = 1f;
|
||||
|
||||
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleName
|
||||
{
|
||||
[DataField("name")] private string _roleName = "Unknown";
|
||||
|
||||
[DataField("description")] private string _roleDescription = "Unknown";
|
||||
|
||||
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
|
||||
|
||||
// TODO ROLE TIMERS
|
||||
// Actually make use of / enforce this requirement?
|
||||
// Why is this even here.
|
||||
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
||||
[DataField("requirements")]
|
||||
public HashSet<JobRequirement>? Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
|
||||
public bool MakeSentient = true;
|
||||
|
||||
/// <summary>
|
||||
/// The probability that this ghost role will be available after init.
|
||||
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
|
||||
/// </summary>
|
||||
[DataField("prob")]
|
||||
public float Probability = 1f;
|
||||
|
||||
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleName
|
||||
get => Loc.GetString(_roleName);
|
||||
set
|
||||
{
|
||||
get => Loc.GetString(_roleName);
|
||||
set
|
||||
{
|
||||
_roleName = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
_roleName = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleDescription
|
||||
{
|
||||
get => Loc.GetString(_roleDescription);
|
||||
set
|
||||
{
|
||||
_roleDescription = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleRules
|
||||
{
|
||||
get => Loc.GetString(_roleRules);
|
||||
set
|
||||
{
|
||||
_roleRules = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("allowSpeech")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AllowSpeech { get; set; } = true;
|
||||
|
||||
[DataField("allowMovement")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AllowMovement { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public bool Taken { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public uint Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reregisters the ghost role when the current player ghosts.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("reregister")]
|
||||
public bool ReregisterOnGhost { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
||||
/// </summary>
|
||||
[DataField("raffle")]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleDescription
|
||||
{
|
||||
get => Loc.GetString(_roleDescription);
|
||||
set
|
||||
{
|
||||
_roleDescription = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleRules
|
||||
{
|
||||
get => Loc.GetString(_roleRules);
|
||||
set
|
||||
{
|
||||
_roleRules = value;
|
||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("allowSpeech")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AllowSpeech { get; set; } = true;
|
||||
|
||||
[DataField("allowMovement")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AllowMovement { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public bool Taken { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public uint Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reregisters the ghost role when the current player ghosts.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("reregister")]
|
||||
public bool ReregisterOnGhost { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
||||
/// </summary>
|
||||
[DataField("raffle")]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,245 +37,244 @@ using Content.Shared.Traits.Assorted;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Content.Shared.Ghost.Roles.Components;
|
||||
|
||||
namespace Content.Server.Zombies
|
||||
namespace Content.Server.Zombies;
|
||||
|
||||
/// <summary>
|
||||
/// Handles zombie propagation and inherent zombie traits
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Don't Shitcode Open Inside
|
||||
/// </remarks>
|
||||
public sealed partial class ZombieSystem
|
||||
{
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Handles zombie propagation and inherent zombie traits
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Don't Shitcode Open Inside
|
||||
/// </remarks>
|
||||
public sealed partial class ZombieSystem
|
||||
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
/// </summary>
|
||||
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
|
||||
if (args.NewMobState == MobState.Dead)
|
||||
{
|
||||
if (args.NewMobState == MobState.Dead)
|
||||
{
|
||||
ZombifyEntity(uid, args.Component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the general purpose function to call if you want to zombify an entity.
|
||||
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
|
||||
/// </summary>
|
||||
/// <param name="target">the entity being zombified</param>
|
||||
/// <param name="mobState"></param>
|
||||
/// <remarks>
|
||||
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
|
||||
/// This function is the god function for zombie stuff, and it is cursed. I have
|
||||
/// attempted to label everything thouroughly for your sanity. I have attempted to
|
||||
/// rewrite this, but this is how it shall lie eternal. Turn back now.
|
||||
/// -emo
|
||||
/// </remarks>
|
||||
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
|
||||
{
|
||||
//Don't zombfiy zombies
|
||||
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
|
||||
return;
|
||||
|
||||
if (!Resolve(target, ref mobState, logMissing: false))
|
||||
return;
|
||||
|
||||
//you're a real zombie now, son.
|
||||
var zombiecomp = AddComp<ZombieComponent>(target);
|
||||
|
||||
//we need to basically remove all of these because zombies shouldn't
|
||||
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
|
||||
RemComp<RespiratorComponent>(target);
|
||||
RemComp<BarotraumaComponent>(target);
|
||||
RemComp<HungerComponent>(target);
|
||||
RemComp<ThirstComponent>(target);
|
||||
RemComp<ReproductiveComponent>(target);
|
||||
RemComp<ReproductivePartnerComponent>(target);
|
||||
RemComp<LegsParalyzedComponent>(target);
|
||||
RemComp<ComplexInteractionComponent>(target);
|
||||
|
||||
//funny voice
|
||||
var accentType = "zombie";
|
||||
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
|
||||
accentType = accent.Accent;
|
||||
|
||||
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
|
||||
|
||||
//This is needed for stupid entities that fuck up combat mode component
|
||||
//in an attempt to make an entity not attack. This is the easiest way to do it.
|
||||
var combat = EnsureComp<CombatModeComponent>(target);
|
||||
RemComp<PacifiedComponent>(target);
|
||||
_combat.SetCanDisarm(target, false, combat);
|
||||
_combat.SetInCombatMode(target, true, combat);
|
||||
|
||||
//This is the actual damage of the zombie. We assign the visual appearance
|
||||
//and range here because of stuff we'll find out later
|
||||
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
||||
melee.Animation = zombiecomp.AttackAnimation;
|
||||
melee.WideAnimation = zombiecomp.AttackAnimation;
|
||||
melee.AltDisarm = false;
|
||||
melee.Range = 1.2f;
|
||||
melee.Angle = 0.0f;
|
||||
melee.HitSound = zombiecomp.BiteSound;
|
||||
|
||||
if (mobState.CurrentState == MobState.Alive)
|
||||
{
|
||||
// Groaning when damaged
|
||||
EnsureComp<EmoteOnDamageComponent>(target);
|
||||
_emoteOnDamage.AddEmote(target, "Scream");
|
||||
|
||||
// Random groaning
|
||||
EnsureComp<AutoEmoteComponent>(target);
|
||||
_autoEmote.AddEmote(target, "ZombieGroan");
|
||||
}
|
||||
|
||||
//We have specific stuff for humanoid zombies because they matter more
|
||||
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
|
||||
{
|
||||
//store some values before changing them in case the humanoid get cloned later
|
||||
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
|
||||
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
|
||||
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
|
||||
if (TryComp<BloodstreamComponent>(target, out var stream))
|
||||
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
|
||||
|
||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
|
||||
|
||||
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
|
||||
huApComp.EyeColor = zombiecomp.EyeColor;
|
||||
|
||||
// this might not resync on clone?
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
|
||||
//This is done here because non-humanoids shouldn't get baller damage
|
||||
//lord forgive me for the hardcoded damage
|
||||
DamageSpecifier dspec = new()
|
||||
{
|
||||
DamageDict = new()
|
||||
{
|
||||
{ "Slash", 13 },
|
||||
{ "Piercing", 7 },
|
||||
{ "Structural", 10 }
|
||||
}
|
||||
};
|
||||
melee.Damage = dspec;
|
||||
|
||||
// humanoid zombies get to pry open doors and shit
|
||||
var pryComp = EnsureComp<PryingComponent>(target);
|
||||
pryComp.SpeedModifier = 0.75f;
|
||||
pryComp.PryPowered = true;
|
||||
pryComp.Force = true;
|
||||
|
||||
Dirty(target, pryComp);
|
||||
}
|
||||
|
||||
Dirty(target, melee);
|
||||
|
||||
//The zombie gets the assigned damage weaknesses and strengths
|
||||
_damageable.SetDamageModifierSetId(target, "Zombie");
|
||||
|
||||
//This makes it so the zombie doesn't take bloodloss damage.
|
||||
//NOTE: they are supposed to bleed, just not take damage
|
||||
_bloodstream.SetBloodLossThreshold(target, 0f);
|
||||
//Give them zombie blood
|
||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
|
||||
|
||||
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
|
||||
_inventory.TryUnequip(target, "gloves", true, true);
|
||||
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
|
||||
_inventory.TryUnequip(target, "ears", true, true);
|
||||
|
||||
//popup
|
||||
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
|
||||
|
||||
//Make it sentient if it's an animal or something
|
||||
MakeSentientCommand.MakeSentient(target, EntityManager);
|
||||
|
||||
//Make the zombie not die in the cold. Good for space zombies
|
||||
if (TryComp<TemperatureComponent>(target, out var tempComp))
|
||||
tempComp.ColdDamage.ClampMax(0);
|
||||
|
||||
//Heals the zombie from all the damage it took while human
|
||||
if (TryComp<DamageableComponent>(target, out var damageablecomp))
|
||||
_damageable.SetAllDamage(target, damageablecomp, 0);
|
||||
_mobState.ChangeMobState(target, MobState.Alive);
|
||||
|
||||
_faction.ClearFactions(target, dirty: false);
|
||||
_faction.AddFaction(target, "Zombie");
|
||||
|
||||
//gives it the funny "Zombie ___" name.
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
|
||||
_identity.QueueIdentityUpdate(target);
|
||||
|
||||
var htn = EnsureComp<HTNComponent>(target);
|
||||
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
|
||||
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
||||
_npc.SleepNPC(target, htn);
|
||||
|
||||
//He's gotta have a mind
|
||||
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
|
||||
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
||||
{
|
||||
//Zombie role for player manifest
|
||||
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
|
||||
|
||||
//Greeting message for new bebe zombers
|
||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||
|
||||
// Notificate player about new role assignment
|
||||
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
|
||||
}
|
||||
else
|
||||
{
|
||||
_npc.WakeNPC(target, htn);
|
||||
}
|
||||
|
||||
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
|
||||
{
|
||||
//yet more hardcoding. Visit zombie.ftl for more information.
|
||||
var ghostRole = EnsureComp<GhostRoleComponent>(target);
|
||||
EnsureComp<GhostTakeoverAvailableComponent>(target);
|
||||
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
||||
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
||||
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
||||
}
|
||||
|
||||
if (TryComp<HandsComponent>(target, out var handsComp))
|
||||
{
|
||||
_hands.RemoveHands(target);
|
||||
RemComp(target, handsComp);
|
||||
}
|
||||
|
||||
// Sloth: What the fuck?
|
||||
// How long until compregistry lmao.
|
||||
RemComp<PullerComponent>(target);
|
||||
|
||||
// No longer waiting to become a zombie:
|
||||
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
|
||||
RemCompDeferred<PendingZombieComponent>(target);
|
||||
|
||||
//zombie gamemode stuff
|
||||
var ev = new EntityZombifiedEvent(target);
|
||||
RaiseLocalEvent(target, ref ev, true);
|
||||
//zombies get slowdown once they convert
|
||||
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
|
||||
ZombifyEntity(uid, args.Component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the general purpose function to call if you want to zombify an entity.
|
||||
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
|
||||
/// </summary>
|
||||
/// <param name="target">the entity being zombified</param>
|
||||
/// <param name="mobState"></param>
|
||||
/// <remarks>
|
||||
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
|
||||
/// This function is the god function for zombie stuff, and it is cursed. I have
|
||||
/// attempted to label everything thouroughly for your sanity. I have attempted to
|
||||
/// rewrite this, but this is how it shall lie eternal. Turn back now.
|
||||
/// -emo
|
||||
/// </remarks>
|
||||
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
|
||||
{
|
||||
//Don't zombfiy zombies
|
||||
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
|
||||
return;
|
||||
|
||||
if (!Resolve(target, ref mobState, logMissing: false))
|
||||
return;
|
||||
|
||||
//you're a real zombie now, son.
|
||||
var zombiecomp = AddComp<ZombieComponent>(target);
|
||||
|
||||
//we need to basically remove all of these because zombies shouldn't
|
||||
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
|
||||
RemComp<RespiratorComponent>(target);
|
||||
RemComp<BarotraumaComponent>(target);
|
||||
RemComp<HungerComponent>(target);
|
||||
RemComp<ThirstComponent>(target);
|
||||
RemComp<ReproductiveComponent>(target);
|
||||
RemComp<ReproductivePartnerComponent>(target);
|
||||
RemComp<LegsParalyzedComponent>(target);
|
||||
RemComp<ComplexInteractionComponent>(target);
|
||||
|
||||
//funny voice
|
||||
var accentType = "zombie";
|
||||
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
|
||||
accentType = accent.Accent;
|
||||
|
||||
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
|
||||
|
||||
//This is needed for stupid entities that fuck up combat mode component
|
||||
//in an attempt to make an entity not attack. This is the easiest way to do it.
|
||||
var combat = EnsureComp<CombatModeComponent>(target);
|
||||
RemComp<PacifiedComponent>(target);
|
||||
_combat.SetCanDisarm(target, false, combat);
|
||||
_combat.SetInCombatMode(target, true, combat);
|
||||
|
||||
//This is the actual damage of the zombie. We assign the visual appearance
|
||||
//and range here because of stuff we'll find out later
|
||||
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
||||
melee.Animation = zombiecomp.AttackAnimation;
|
||||
melee.WideAnimation = zombiecomp.AttackAnimation;
|
||||
melee.AltDisarm = false;
|
||||
melee.Range = 1.2f;
|
||||
melee.Angle = 0.0f;
|
||||
melee.HitSound = zombiecomp.BiteSound;
|
||||
|
||||
if (mobState.CurrentState == MobState.Alive)
|
||||
{
|
||||
// Groaning when damaged
|
||||
EnsureComp<EmoteOnDamageComponent>(target);
|
||||
_emoteOnDamage.AddEmote(target, "Scream");
|
||||
|
||||
// Random groaning
|
||||
EnsureComp<AutoEmoteComponent>(target);
|
||||
_autoEmote.AddEmote(target, "ZombieGroan");
|
||||
}
|
||||
|
||||
//We have specific stuff for humanoid zombies because they matter more
|
||||
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
|
||||
{
|
||||
//store some values before changing them in case the humanoid get cloned later
|
||||
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
|
||||
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
|
||||
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
|
||||
if (TryComp<BloodstreamComponent>(target, out var stream))
|
||||
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
|
||||
|
||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
|
||||
|
||||
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
|
||||
huApComp.EyeColor = zombiecomp.EyeColor;
|
||||
|
||||
// this might not resync on clone?
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
|
||||
//This is done here because non-humanoids shouldn't get baller damage
|
||||
//lord forgive me for the hardcoded damage
|
||||
DamageSpecifier dspec = new()
|
||||
{
|
||||
DamageDict = new()
|
||||
{
|
||||
{ "Slash", 13 },
|
||||
{ "Piercing", 7 },
|
||||
{ "Structural", 10 }
|
||||
}
|
||||
};
|
||||
melee.Damage = dspec;
|
||||
|
||||
// humanoid zombies get to pry open doors and shit
|
||||
var pryComp = EnsureComp<PryingComponent>(target);
|
||||
pryComp.SpeedModifier = 0.75f;
|
||||
pryComp.PryPowered = true;
|
||||
pryComp.Force = true;
|
||||
|
||||
Dirty(target, pryComp);
|
||||
}
|
||||
|
||||
Dirty(target, melee);
|
||||
|
||||
//The zombie gets the assigned damage weaknesses and strengths
|
||||
_damageable.SetDamageModifierSetId(target, "Zombie");
|
||||
|
||||
//This makes it so the zombie doesn't take bloodloss damage.
|
||||
//NOTE: they are supposed to bleed, just not take damage
|
||||
_bloodstream.SetBloodLossThreshold(target, 0f);
|
||||
//Give them zombie blood
|
||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
|
||||
|
||||
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
|
||||
_inventory.TryUnequip(target, "gloves", true, true);
|
||||
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
|
||||
_inventory.TryUnequip(target, "ears", true, true);
|
||||
|
||||
//popup
|
||||
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
|
||||
|
||||
//Make it sentient if it's an animal or something
|
||||
MakeSentientCommand.MakeSentient(target, EntityManager);
|
||||
|
||||
//Make the zombie not die in the cold. Good for space zombies
|
||||
if (TryComp<TemperatureComponent>(target, out var tempComp))
|
||||
tempComp.ColdDamage.ClampMax(0);
|
||||
|
||||
//Heals the zombie from all the damage it took while human
|
||||
if (TryComp<DamageableComponent>(target, out var damageablecomp))
|
||||
_damageable.SetAllDamage(target, damageablecomp, 0);
|
||||
_mobState.ChangeMobState(target, MobState.Alive);
|
||||
|
||||
_faction.ClearFactions(target, dirty: false);
|
||||
_faction.AddFaction(target, "Zombie");
|
||||
|
||||
//gives it the funny "Zombie ___" name.
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
|
||||
_identity.QueueIdentityUpdate(target);
|
||||
|
||||
var htn = EnsureComp<HTNComponent>(target);
|
||||
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
|
||||
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
||||
_npc.SleepNPC(target, htn);
|
||||
|
||||
//He's gotta have a mind
|
||||
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
|
||||
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
||||
{
|
||||
//Zombie role for player manifest
|
||||
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
|
||||
|
||||
//Greeting message for new bebe zombers
|
||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||
|
||||
// Notificate player about new role assignment
|
||||
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
|
||||
}
|
||||
else
|
||||
{
|
||||
_npc.WakeNPC(target, htn);
|
||||
}
|
||||
|
||||
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
|
||||
{
|
||||
//yet more hardcoding. Visit zombie.ftl for more information.
|
||||
var ghostRole = EnsureComp<GhostRoleComponent>(target);
|
||||
EnsureComp<GhostTakeoverAvailableComponent>(target);
|
||||
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
||||
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
||||
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
||||
}
|
||||
|
||||
if (TryComp<HandsComponent>(target, out var handsComp))
|
||||
{
|
||||
_hands.RemoveHands(target);
|
||||
RemComp(target, handsComp);
|
||||
}
|
||||
|
||||
// Sloth: What the fuck?
|
||||
// How long until compregistry lmao.
|
||||
RemComp<PullerComponent>(target);
|
||||
|
||||
// No longer waiting to become a zombie:
|
||||
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
|
||||
RemCompDeferred<PendingZombieComponent>(target);
|
||||
|
||||
//zombie gamemode stuff
|
||||
var ev = new EntityZombifiedEvent(target);
|
||||
RaiseLocalEvent(target, ref ev, true);
|
||||
//zombies get slowdown once they convert
|
||||
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Administration.Events
|
||||
namespace Content.Shared.Administration.Events;
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public sealed class PlayerInfoChangedEvent : EntityEventArgs
|
||||
{
|
||||
[NetSerializable, Serializable]
|
||||
public sealed class PlayerInfoChangedEvent : EntityEventArgs
|
||||
{
|
||||
public PlayerInfo? PlayerInfo;
|
||||
}
|
||||
public PlayerInfo? PlayerInfo;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Administration
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed record PlayerInfo(
|
||||
string Username,
|
||||
string CharacterName,
|
||||
string IdentityName,
|
||||
string StartingJob,
|
||||
bool Antag,
|
||||
NetEntity? NetEntity,
|
||||
NetUserId SessionId,
|
||||
bool Connected,
|
||||
bool ActiveThisRound,
|
||||
TimeSpan? OverallPlaytime)
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed record PlayerInfo(
|
||||
string Username,
|
||||
string CharacterName,
|
||||
string IdentityName,
|
||||
string StartingJob,
|
||||
bool Antag,
|
||||
NetEntity? NetEntity,
|
||||
NetUserId SessionId,
|
||||
bool Connected,
|
||||
bool ActiveThisRound,
|
||||
TimeSpan? OverallPlaytime)
|
||||
private string? _playtimeString;
|
||||
|
||||
public bool IsPinned { get; set; }
|
||||
|
||||
public string PlaytimeString => _playtimeString ??=
|
||||
OverallPlaytime?.ToString("%d':'hh':'mm") ?? Loc.GetString("generic-unknown-title");
|
||||
|
||||
public bool Equals(PlayerInfo? other)
|
||||
{
|
||||
private string? _playtimeString;
|
||||
return other?.SessionId == SessionId;
|
||||
}
|
||||
|
||||
public bool IsPinned { get; set; }
|
||||
|
||||
public string PlaytimeString => _playtimeString ??=
|
||||
OverallPlaytime?.ToString("%d':'hh':'mm") ?? Loc.GetString("generic-unknown-title");
|
||||
|
||||
public bool Equals(PlayerInfo? other)
|
||||
{
|
||||
return other?.SessionId == SessionId;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return SessionId.GetHashCode();
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return SessionId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Ghost.Roles
|
||||
namespace Content.Shared.Ghost.Roles;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GhostRole
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GhostRole
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public NetEntity Id;
|
||||
}
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public NetEntity Id;
|
||||
}
|
||||
|
||||
@@ -1,97 +1,96 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Mind.Components
|
||||
namespace Content.Shared.Mind.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component indicates that this entity may have mind, which is simply an entity with a <see cref="MindComponent"/>.
|
||||
/// The mind entity is not actually stored in a "container", but is simply stored in nullspace.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class MindContainerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// This component indicates that this entity may have mind, which is simply an entity with a <see cref="MindComponent"/>.
|
||||
/// The mind entity is not actually stored in a "container", but is simply stored in nullspace.
|
||||
/// The mind controlling this mob. Can be null.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class MindContainerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The mind controlling this mob. Can be null.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public EntityUid? Mind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if we have a mind, false otherwise.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Mind))]
|
||||
public bool HasMind => Mind != null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether examining should show information about the mind or not.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("showExamineInfo"), AutoNetworkedField]
|
||||
public bool ShowExamineInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mind will be put on a ghost after this component is shutdown.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("ghostOnShutdown")]
|
||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public bool GhostOnShutdown { get; set; } = true;
|
||||
}
|
||||
|
||||
public abstract class MindEvent : EntityEventArgs
|
||||
{
|
||||
public readonly Entity<MindComponent> Mind;
|
||||
public readonly Entity<MindContainerComponent> Container;
|
||||
|
||||
public MindEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
{
|
||||
Mind = mind;
|
||||
Container = container;
|
||||
}
|
||||
}
|
||||
[DataField, AutoNetworkedField]
|
||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public EntityUid? Mind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind-container when a mind gets removed.
|
||||
/// True if we have a mind, false otherwise.
|
||||
/// </summary>
|
||||
public sealed class MindRemovedMessage : MindEvent
|
||||
{
|
||||
public MindRemovedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
[MemberNotNullWhen(true, nameof(Mind))]
|
||||
public bool HasMind => Mind != null;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind when it gets removed from a mind-container.
|
||||
/// Whether examining should show information about the mind or not.
|
||||
/// </summary>
|
||||
public sealed class MindGotRemovedEvent : MindEvent
|
||||
{
|
||||
public MindGotRemovedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("showExamineInfo"), AutoNetworkedField]
|
||||
public bool ShowExamineInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind-container when a mind gets added.
|
||||
/// Whether the mind will be put on a ghost after this component is shutdown.
|
||||
/// </summary>
|
||||
public sealed class MindAddedMessage : MindEvent
|
||||
{
|
||||
public MindAddedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("ghostOnShutdown")]
|
||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public bool GhostOnShutdown { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind when it gets added to a mind-container.
|
||||
/// </summary>
|
||||
public sealed class MindGotAddedEvent : MindEvent
|
||||
public abstract class MindEvent : EntityEventArgs
|
||||
{
|
||||
public readonly Entity<MindComponent> Mind;
|
||||
public readonly Entity<MindContainerComponent> Container;
|
||||
|
||||
public MindEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
{
|
||||
Mind = mind;
|
||||
Container = container;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind-container when a mind gets removed.
|
||||
/// </summary>
|
||||
public sealed class MindRemovedMessage : MindEvent
|
||||
{
|
||||
public MindRemovedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind when it gets removed from a mind-container.
|
||||
/// </summary>
|
||||
public sealed class MindGotRemovedEvent : MindEvent
|
||||
{
|
||||
public MindGotRemovedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind-container when a mind gets added.
|
||||
/// </summary>
|
||||
public sealed class MindAddedMessage : MindEvent
|
||||
{
|
||||
public MindAddedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a mind when it gets added to a mind-container.
|
||||
/// </summary>
|
||||
public sealed class MindGotAddedEvent : MindEvent
|
||||
{
|
||||
public MindGotAddedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
public MindGotAddedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||
: base(mind, container)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,105 +5,104 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Mind
|
||||
namespace Content.Shared.Mind;
|
||||
|
||||
/// <summary>
|
||||
/// This component stores information about a player/mob mind. The component will be attached to a mind-entity
|
||||
/// which is stored in null-space. The entity that is currently "possessed" by the mind will have a
|
||||
/// <see cref="MindContainerComponent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Roles are attached as components on the mind-entity entity.
|
||||
/// Think of it like this: if a player is supposed to have their memories,
|
||||
/// their mind follows along.
|
||||
///
|
||||
/// Things such as respawning do not follow, because you're a new character.
|
||||
/// Getting borged, cloned, turned into a catbeast, etc... will keep it following you.
|
||||
///
|
||||
/// Minds are stored in null-space, and are thus generally not set to players unless that player is the owner
|
||||
/// of the mind. As a result it should be safe to network "secret" information like roles & objectives
|
||||
/// </remarks>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
public sealed partial class MindComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<EntityUid> Objectives = new();
|
||||
|
||||
/// <summary>
|
||||
/// This component stores information about a player/mob mind. The component will be attached to a mind-entity
|
||||
/// which is stored in null-space. The entity that is currently "possessed" by the mind will have a
|
||||
/// <see cref="MindContainerComponent"/>.
|
||||
/// The session ID of the player owning this mind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Roles are attached as components on the mind-entity entity.
|
||||
/// Think of it like this: if a player is supposed to have their memories,
|
||||
/// their mind follows along.
|
||||
///
|
||||
/// Things such as respawning do not follow, because you're a new character.
|
||||
/// Getting borged, cloned, turned into a catbeast, etc... will keep it following you.
|
||||
///
|
||||
/// Minds are stored in null-space, and are thus generally not set to players unless that player is the owner
|
||||
/// of the mind. As a result it should be safe to network "secret" information like roles & objectives
|
||||
/// </remarks>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
public sealed partial class MindComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<EntityUid> Objectives = new();
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public NetUserId? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The session ID of the player owning this mind.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public NetUserId? UserId { get; set; }
|
||||
/// <summary>
|
||||
/// The session ID of the original owner, if any.
|
||||
/// May end up used for round-end information (as the owner may have abandoned Mind since)
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public NetUserId? OriginalOwnerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The session ID of the original owner, if any.
|
||||
/// May end up used for round-end information (as the owner may have abandoned Mind since)
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public NetUserId? OriginalOwnerUserId { get; set; }
|
||||
/// <summary>
|
||||
/// The first entity that this mind controlled. Used for round end information.
|
||||
/// Might be relevant if the player has ghosted since.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public NetEntity? OriginalOwnedEntity;
|
||||
// This is a net entity, because this field currently ddoes not get set to null when this entity is deleted.
|
||||
// This is a lazy way to ensure that people check that the entity still exists.
|
||||
// TODO MIND Fix this properly by adding an OriginalMindContainerComponent or something like that.
|
||||
|
||||
/// <summary>
|
||||
/// The first entity that this mind controlled. Used for round end information.
|
||||
/// Might be relevant if the player has ghosted since.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public NetEntity? OriginalOwnedEntity;
|
||||
// This is a net entity, because this field currently ddoes not get set to null when this entity is deleted.
|
||||
// This is a lazy way to ensure that people check that the entity still exists.
|
||||
// TODO MIND Fix this properly by adding an OriginalMindContainerComponent or something like that.
|
||||
[ViewVariables]
|
||||
public bool IsVisitingEntity => VisitingEntity != null;
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsVisitingEntity => VisitingEntity != null;
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public EntityUid? VisitingEntity { get; set; }
|
||||
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public EntityUid? VisitingEntity { get; set; }
|
||||
[ViewVariables]
|
||||
public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity;
|
||||
|
||||
[ViewVariables]
|
||||
public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity;
|
||||
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? CharacterName { get; set; }
|
||||
|
||||
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? CharacterName { get; set; }
|
||||
/// <summary>
|
||||
/// The time of death for this Mind.
|
||||
/// Can be null - will be null if the Mind is not considered "dead".
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan? TimeOfDeath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time of death for this Mind.
|
||||
/// Can be null - will be null if the Mind is not considered "dead".
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan? TimeOfDeath { get; set; }
|
||||
/// <summary>
|
||||
/// The entity currently owned by this mind.
|
||||
/// Can be null.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public EntityUid? OwnedEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The entity currently owned by this mind.
|
||||
/// Can be null.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||
public EntityUid? OwnedEntity { get; set; }
|
||||
/// <summary>
|
||||
/// An enumerable over all the objective entities this mind has.
|
||||
/// </summary>
|
||||
[ViewVariables, Obsolete("Use Objectives field")]
|
||||
public IEnumerable<EntityUid> AllObjectives => Objectives;
|
||||
|
||||
/// <summary>
|
||||
/// An enumerable over all the objective entities this mind has.
|
||||
/// </summary>
|
||||
[ViewVariables, Obsolete("Use Objectives field")]
|
||||
public IEnumerable<EntityUid> AllObjectives => Objectives;
|
||||
/// <summary>
|
||||
/// Prevents user from ghosting out
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("preventGhosting")]
|
||||
public bool PreventGhosting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prevents user from ghosting out
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("preventGhosting")]
|
||||
public bool PreventGhosting { get; set; }
|
||||
/// <summary>
|
||||
/// Prevents user from suiciding
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("preventSuicide")]
|
||||
public bool PreventSuicide { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prevents user from suiciding
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("preventSuicide")]
|
||||
public bool PreventSuicide { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The session of the player owning this mind.
|
||||
/// Can be null, in which case the player is currently not logged in.
|
||||
/// </summary>
|
||||
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
|
||||
// TODO remove this after moving IPlayerManager functions to shared
|
||||
public ICommonSession? Session { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The session of the player owning this mind.
|
||||
/// Can be null, in which case the player is currently not logged in.
|
||||
/// </summary>
|
||||
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
|
||||
// TODO remove this after moving IPlayerManager functions to shared
|
||||
public ICommonSession? Session { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user