diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
index 81e8916949..4691a9da9f 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
@@ -8,5 +8,6 @@
+
diff --git a/Content.Client/Chat/ChatHelper.cs b/Content.Client/Chat/ChatHelper.cs
index 275c7d2069..fdf4544b1c 100644
--- a/Content.Client/Chat/ChatHelper.cs
+++ b/Content.Client/Chat/ChatHelper.cs
@@ -10,7 +10,8 @@ namespace Content.Client.Chat
{
ChatChannel.Server => Color.Orange,
ChatChannel.Radio => Color.LimeGreen,
- ChatChannel.OOC => Color.LightSkyBlue,
+ ChatChannel.LOOC => Color.LightSkyBlue,
+ ChatChannel.OOC => Color.RoyalBlue,
ChatChannel.Dead => Color.MediumPurple,
ChatChannel.Admin => Color.Red,
_ => Color.DarkGray
diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs
index 60ea578aa1..5088ae9b28 100644
--- a/Content.Client/Chat/Managers/ChatManager.cs
+++ b/Content.Client/Chat/Managers/ChatManager.cs
@@ -188,6 +188,8 @@ namespace Content.Client.Chat.Managers
// can always send/recieve OOC
SelectableChannels |= ChatSelectChannel.OOC;
FilterableChannels |= ChatChannel.OOC;
+ SelectableChannels |= ChatSelectChannel.LOOC;
+ FilterableChannels |= ChatChannel.LOOC;
// can always hear server (nobody can actually send server messages).
FilterableChannels |= ChatChannel.Server;
@@ -318,6 +320,10 @@ namespace Content.Client.Chat.Managers
_consoleHost.ExecuteCommand(text.ToString());
break;
+ case ChatSelectChannel.LOOC:
+ _consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
+ break;
+
case ChatSelectChannel.OOC:
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
break;
diff --git a/Content.Client/Chat/UI/ChatBox.xaml.cs b/Content.Client/Chat/UI/ChatBox.xaml.cs
index 0199d0666a..945d8e494d 100644
--- a/Content.Client/Chat/UI/ChatBox.xaml.cs
+++ b/Content.Client/Chat/UI/ChatBox.xaml.cs
@@ -44,6 +44,7 @@ namespace Content.Client.Chat.UI
ChatSelectChannel.Local,
ChatSelectChannel.Emotes,
ChatSelectChannel.Radio,
+ ChatSelectChannel.LOOC,
ChatSelectChannel.OOC,
ChatSelectChannel.Dead,
ChatSelectChannel.Admin
@@ -491,7 +492,8 @@ namespace Content.Client.Chat.UI
return channel switch
{
ChatSelectChannel.Radio => Color.LimeGreen,
- ChatSelectChannel.OOC => Color.LightSkyBlue,
+ ChatSelectChannel.LOOC => Color.LightSkyBlue,
+ ChatSelectChannel.OOC => Color.RoyalBlue,
ChatSelectChannel.Dead => Color.MediumPurple,
ChatSelectChannel.Admin => Color.Red,
_ => Color.DarkGray
diff --git a/Content.Server/Chat/Commands/LOOCCommand.cs b/Content.Server/Chat/Commands/LOOCCommand.cs
new file mode 100644
index 0000000000..5d627eee15
--- /dev/null
+++ b/Content.Server/Chat/Commands/LOOCCommand.cs
@@ -0,0 +1,56 @@
+using Content.Server.Administration;
+using Content.Server.Chat.Managers;
+using Content.Server.Players;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.Enums;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Chat.Commands
+{
+ [AnyCommand]
+ internal class LOOCCommand : IConsoleCommand
+ {
+ public string Command => "looc";
+ public string Description => "Send Local Out Of Character chat messages.";
+ public string Help => "looc ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var player = shell.Player as IPlayerSession;
+ if (player == null)
+ {
+ shell.WriteLine("This command cannot be run from the server.");
+ return;
+ }
+
+ if (player.Status != SessionStatus.InGame || player.AttachedEntity == null)
+ return;
+
+ if (args.Length < 1)
+ return;
+
+ var message = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(message))
+ return;
+
+ var chat = IoCManager.Resolve();
+ var mindComponent = player.ContentData()?.Mind;
+
+ if (mindComponent == null)
+ {
+ shell.WriteError("You don't have a mind!");
+ return;
+ }
+
+ if (mindComponent.OwnedEntity == null)
+ {
+ shell.WriteError("You don't have an entity!");
+ return;
+ }
+
+ chat.EntityLOOC(mindComponent.OwnedEntity.Value, message);
+ }
+ }
+}
diff --git a/Content.Server/Chat/Commands/SetLOOCCommand.cs b/Content.Server/Chat/Commands/SetLOOCCommand.cs
new file mode 100644
index 0000000000..3ed206b054
--- /dev/null
+++ b/Content.Server/Chat/Commands/SetLOOCCommand.cs
@@ -0,0 +1,44 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.Console;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Chat.Commands;
+
+[AdminCommand(AdminFlags.Server)]
+public class SetLOOCCommand : IConsoleCommand
+{
+ public string Command => "setlooc";
+ public string Description => Loc.GetString("set-looc-command-description");
+ public string Help => Loc.GetString("set-looc-command-help");
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var cfg = IoCManager.Resolve();
+
+ if (args.Length > 1)
+ {
+ shell.WriteError(Loc.GetString("set-looc-command-too-many-arguments-error"));
+ return;
+ }
+
+ var looc = cfg.GetCVar(CCVars.LoocEnabled);
+
+ if (args.Length == 0)
+ {
+ looc = !looc;
+ }
+
+ if (args.Length == 1 && !bool.TryParse(args[0], out looc))
+ {
+ shell.WriteError(Loc.GetString("set-looc-command-invalid-argument-error"));
+ return;
+ }
+
+ cfg.SetCVar(CCVars.LoocEnabled, looc);
+
+ shell.WriteLine(Loc.GetString(looc ? "set-looc-command-looc-enabled" : "set-looc-command-looc-disabled"));
+ }
+}
diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs
index cb1576e6e9..18df74e465 100644
--- a/Content.Server/Chat/Managers/ChatManager.cs
+++ b/Content.Server/Chat/Managers/ChatManager.cs
@@ -59,12 +59,15 @@ namespace Content.Server.Chat.Managers
private readonly List _chatTransformHandlers = new();
private bool _oocEnabled = true;
private bool _adminOocEnabled = true;
+ private bool _loocEnabled = true;
+ private bool _adminLoocEnabled = true;
public void Initialize()
{
_netManager.RegisterNetMessage();
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
+ _configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
}
@@ -74,6 +77,12 @@ namespace Content.Server.Chat.Managers
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
}
+ private void OnLoocEnabledChanged(bool val)
+ {
+ _loocEnabled = val;
+ DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-looc-chat-enabled-message" : "chat-manager-looc-chat-disabled-message"));
+ }
+
private void OnAdminOocEnabledChanged(bool val)
{
_adminOocEnabled = val;
@@ -238,6 +247,47 @@ namespace Content.Server.Chat.Managers
_netManager.ServerSendToMany(msg, clients);
}
+ public void EntityLOOC(EntityUid source, string message)
+ {
+ // Check if entity is a player
+ if (!_entManager.TryGetComponent(source, out ActorComponent? actor))
+ {
+ return;
+ }
+
+ if (_adminManager.IsAdmin(actor.PlayerSession))
+ {
+ if (!_adminLoocEnabled)
+ {
+ return;
+ }
+ }
+ else if (!_loocEnabled)
+ {
+ return;
+ }
+
+ // Check if message exceeds the character limit
+ if (message.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(actor.PlayerSession, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
+ return;
+ }
+
+ message = FormattedMessage.EscapeText(message);
+
+ var clients = Filter.Empty()
+ .AddInRange(_entManager.GetComponent(source).MapPosition, VoiceRange)
+ .Recipients
+ .Select(p => p.ConnectedClient)
+ .ToList();
+
+ var msg = _netManager.CreateNetMessage();
+ msg.Channel = ChatChannel.LOOC;
+ msg.Message = message;
+ msg.MessageWrap = Loc.GetString("chat-manager-entity-looc-wrap-message", ("entityName", Name: _entManager.GetComponent(source).EntityName));
+ _netManager.ServerSendToMany(msg, clients);
+ }
public void SendOOC(IPlayerSession player, string message)
{
if (_adminManager.IsAdmin(player))
diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs
index abf7cd2032..2c533aa8dc 100644
--- a/Content.Server/Chat/Managers/IChatManager.cs
+++ b/Content.Server/Chat/Managers/IChatManager.cs
@@ -25,6 +25,7 @@ namespace Content.Server.Chat.Managers
/// If true, message will not be logged to chat boxes but will still produce a speech bubble.
void EntitySay(EntityUid source, string message, bool hideChat=false);
void EntityMe(EntityUid source, string action);
+ void EntityLOOC(EntityUid source, string message);
void SendOOC(IPlayerSession player, string message);
void SendAdminChat(IPlayerSession player, string message);
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index a9de00a556..1b6bda71b8 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -485,6 +485,15 @@ namespace Content.Shared.CCVar
public static readonly CVarDef AdminOocEnabled =
CVarDef.Create("ooc.enabled_admin", true, CVar.NOTIFY);
+ /*
+ * LOOC
+ */
+
+ public static readonly CVarDef LoocEnabled = CVarDef.Create("looc.enabled", true, CVar.NOTIFY);
+
+ public static readonly CVarDef AdminLoocEnabled =
+ CVarDef.Create("looc.enabled_admin", true, CVar.NOTIFY);
+
/*
* Entity Menu Grouping Types
*/
diff --git a/Content.Shared/Chat/ChatChannel.cs b/Content.Shared/Chat/ChatChannel.cs
index fb93d172db..311cadc7ec 100644
--- a/Content.Shared/Chat/ChatChannel.cs
+++ b/Content.Shared/Chat/ChatChannel.cs
@@ -30,36 +30,41 @@ namespace Content.Shared.Chat
///
Radio = 1 << 3,
+ ///
+ /// Local out-of-character channel
+ ///
+ LOOC = 1 << 4,
+
///
/// Out-of-character channel
///
- OOC = 1 << 4,
+ OOC = 1 << 5,
///
/// Visual events the player can see.
/// Basically like visual_message in SS13.
///
- Visual = 1 << 5,
+ Visual = 1 << 6,
///
/// Emotes
///
- Emotes = 1 << 6,
+ Emotes = 1 << 7,
///
/// Deadchat
///
- Dead = 1 << 7,
+ Dead = 1 << 8,
///
/// Admin chat
///
- Admin = 1 << 8,
+ Admin = 1 << 9,
///
/// Unspecified.
///
- Unspecified = 1 << 9,
+ Unspecified = 1 << 10,
///
/// Channels considered to be IC.
diff --git a/Content.Shared/Chat/ChatSelectChannel.cs b/Content.Shared/Chat/ChatSelectChannel.cs
index 56d70cf1d1..1f7d6bb362 100644
--- a/Content.Shared/Chat/ChatSelectChannel.cs
+++ b/Content.Shared/Chat/ChatSelectChannel.cs
@@ -23,6 +23,11 @@ namespace Content.Shared.Chat
///
Radio = ChatChannel.Radio,
+ ///
+ /// Local out-of-character channel
+ ///
+ LOOC = ChatChannel.LOOC,
+
///
/// Out-of-character channel
///
diff --git a/Resources/Locale/en-US/administration/commands/set-looc-command.ftl b/Resources/Locale/en-US/administration/commands/set-looc-command.ftl
new file mode 100644
index 0000000000..2125939052
--- /dev/null
+++ b/Resources/Locale/en-US/administration/commands/set-looc-command.ftl
@@ -0,0 +1,6 @@
+set-looc-command-description = Allows you to enable or disable LOOC.
+set-looc-command-help = Usage: setlooc OR setlooc [value]
+set-looc-command-too-many-arguments-error = Too many arguments.
+set-looc-command-invalid-argument-error = Invalid argument.
+set-looc-command-looc-enabled = LOOC chat has been enabled.
+set-looc-command-looc-disabled = LOOC chat has been disabled.
diff --git a/Resources/Locale/en-US/administration/ui/tabs/server-tab.ftl b/Resources/Locale/en-US/administration/ui/tabs/server-tab.ftl
index 581a2a1c87..41999beaf0 100644
--- a/Resources/Locale/en-US/administration/ui/tabs/server-tab.ftl
+++ b/Resources/Locale/en-US/administration/ui/tabs/server-tab.ftl
@@ -1,3 +1,4 @@
server-reboot = Reboot
server-shutdown = Shutdown
server-ooc-toggle = Toggle OOC
+server-looc-toggle = Toggle LOOC
diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl
index 01b43a67c6..91544a604b 100644
--- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl
+++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl
@@ -3,6 +3,8 @@
chat-manager-max-message-length = Your message exceeds {$maxMessageLength} character limit
chat-manager-ooc-chat-enabled-message = OOC chat has been enabled.
chat-manager-ooc-chat-disabled-message = OOC chat has been disabled.
+chat-manager-looc-chat-enabled-message = LOOC chat has been enabled.
+chat-manager-looc-chat-disabled-message = LOOC chat has been disabled.
chat-manager-admin-ooc-chat-enabled-message = Admin OOC chat has been enabled.
chat-manager-admin-ooc-chat-disabled-message = Admin OOC chat has been disabled.
chat-manager-max-message-length-exceeded-message = Your message exceeded {$limit} character limit
@@ -12,6 +14,7 @@ chat-manager-sender-announcement-wrap-message = {$sender} Announcement:
{"{0}"}
chat-manager-entity-say-wrap-message = {$entityName} says: "{"{0}"}"
chat-manager-entity-me-wrap-message = {$entityName} {"{0}"}
+chat-manager-entity-looc-wrap-message = LOOC: {$entityName}: {"{0}"}
chat-manager-send-ooc-wrap-message = OOC: {$playerName}: {"{0}"}
chat-manager-send-ooc-patron-wrap-message = OOC: [color={$patronColor}]{$playerName}[/color]: {"{0}"}
chat-manager-send-dead-chat-wrap-message = {$deadChannelName}: {$playerName}: {"{0}"}
diff --git a/Resources/Locale/en-US/chat/ui/chat-box.ftl b/Resources/Locale/en-US/chat/ui/chat-box.ftl
index 84a7d09283..b78117df8f 100644
--- a/Resources/Locale/en-US/chat/ui/chat-box.ftl
+++ b/Resources/Locale/en-US/chat/ui/chat-box.ftl
@@ -6,6 +6,7 @@ hud-chatbox-select-channel-Console = Console
hud-chatbox-select-channel-Dead = Dead
hud-chatbox-select-channel-Emotes = Emotes
hud-chatbox-select-channel-Local = Local
+hud-chatbox-select-channel-LOOC = LOOC
hud-chatbox-select-channel-OOC = OOC
hud-chatbox-select-channel-Radio = Radio
@@ -13,6 +14,7 @@ hud-chatbox-channel-Admin = Admin
hud-chatbox-channel-Dead = Dead
hud-chatbox-channel-Emotes = Emotes
hud-chatbox-channel-Local = Local
+hud-chatbox-channel-LOOC = LOOC
hud-chatbox-channel-OOC = OOC
hud-chatbox-channel-Radio = Radio
hud-chatbox-channel-Server = Server