# Conflicts: # .github/CODEOWNERS # .github/workflows/publish.yml # Content.Client/Administration/UI/BanPanel/BanPanel.xaml # Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs # Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs # Content.Client/Entry/EntryPoint.cs # Content.Client/Hands/Systems/HandsSystem.cs # Content.Client/Humanoid/HumanoidAppearanceSystem.cs # Content.Client/Humanoid/SingleMarkingPicker.xaml.cs # Content.Client/IoC/ClientContentIoC.cs # Content.Client/Lobby/LobbyState.cs # Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs # Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs # Content.Client/Lobby/UI/LobbyGui.xaml # Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml # Content.Client/Options/UI/EscapeMenu.xaml # Content.Client/Stealth/StealthSystem.cs # Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs # Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs # Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs # Content.Client/Weapons/Ranged/Systems/GunSystem.cs # Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs # Content.IntegrationTests/Tests/Damageable/DamageableTest.cs # Content.IntegrationTests/Tests/PostMapInitTest.cs # Content.Server/Administration/Commands/AdminWhoCommand.cs # Content.Server/Administration/Commands/PardonCommand.cs # Content.Server/Administration/Commands/RoleUnbanCommand.cs # Content.Server/Administration/Commands/SetOutfitCommand.cs # Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs # Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs # Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs # Content.Server/Body/Components/BloodstreamComponent.cs # Content.Server/Body/Systems/BloodstreamSystem.cs # Content.Server/Body/Systems/MetabolizerSystem.cs # Content.Server/Body/Systems/RespiratorSystem.cs # Content.Server/Chat/Managers/ChatSanitizationManager.cs # Content.Server/Chat/Systems/ChatSystem.Emote.cs # Content.Server/Chat/Systems/ChatSystem.cs # Content.Server/CrewManifest/CrewManifestSystem.cs # Content.Server/Emp/EmpSystem.cs # Content.Server/Explosion/EntitySystems/TriggerSystem.cs # Content.Server/Fax/FaxSystem.cs # Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs # Content.Server/GameTicking/Commands/ForceMapCommand.cs # Content.Server/GameTicking/Commands/ForcePresetCommand.cs # Content.Server/GameTicking/Presets/GamePresetPrototype.cs # Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs # Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs # Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs # Content.Server/Ghost/Roles/GhostRoleSystem.cs # Content.Server/Implants/Components/ScramImplantComponent.cs # Content.Server/Implants/SubdermalImplantSystem.cs # Content.Server/Materials/ProduceMaterialExtractorSystem.cs # Content.Server/Medical/HealthAnalyzerSystem.cs # Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs # Content.Server/NPC/Systems/NPCSystem.cs # Content.Server/Polymorph/Systems/PolymorphSystem.cs # Content.Server/Radio/EntitySystems/HeadsetSystem.cs # Content.Server/Repairable/RepairableSystem.cs # Content.Server/Revenant/EntitySystems/RevenantSystem.cs # Content.Server/RoundEnd/RoundEndSystem.cs # Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs # Content.Server/Speech/EntitySystems/FrenchAccentSystem.cs # Content.Server/Speech/EntitySystems/FrontalLispSystem.cs # Content.Server/Speech/EntitySystems/LizardAccentSystem.cs # Content.Server/Speech/EntitySystems/MothAccentSystem.cs # Content.Server/Speech/EntitySystems/OwOAccentSystem.cs # Content.Server/Station/Systems/StationSpawningSystem.cs # Content.Server/StationEvents/Events/MeteorSwarmSystem.cs # Content.Server/Stunnable/Components/StunOnCollideComponent.cs # Content.Server/SurveillanceCamera/Components/SurveillanceCameraRouterComponent.cs # Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs # Content.Server/Zombies/ZombieSystem.Transform.cs # Content.Server/Zombies/ZombieSystem.cs # Content.Shared/Actions/SharedActionsSystem.cs # Content.Shared/Bed/SharedBedSystem.cs # Content.Shared/Blocking/BlockingSystem.cs # Content.Shared/Body/Systems/SharedBodySystem.Parts.cs # Content.Shared/Chemistry/Components/SolutionRegenerationComponent.cs # Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs # Content.Shared/Construction/Conditions/TileNotBlocked.cs # Content.Shared/Cuffs/SharedCuffableSystem.cs # Content.Shared/Doors/Systems/SharedDoorSystem.cs # Content.Shared/Emoting/EmoteAttemptEvent.cs # Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs # Content.Shared/Eye/VisibilityFlags.cs # Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs # Content.Shared/Humanoid/HumanoidCharacterAppearance.cs # Content.Shared/Interaction/SmartEquipSystem.cs # Content.Shared/Inventory/InventorySystem.Relay.cs # Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs # Content.Shared/Medical/Healing/HealingSystem.cs # Content.Shared/Medical/HealingComponent.cs # Content.Shared/Nutrition/EntitySystems/FoodSystem.cs # Content.Shared/Slippery/SlipperySystem.cs # Content.Shared/SprayPainter/SharedSprayPainterSystem.cs # Content.Shared/Standing/StandingStateComponent.cs # Content.Shared/Standing/StandingStateSystem.cs # Content.Shared/Station/SharedStationSpawningSystem.cs # Content.Shared/Stunnable/SharedStunSystem.cs # Content.Shared/SurveillanceCamera/SharedSurveillanceCameraMonitorSystem.cs # Content.Shared/VendingMachines/VendingMachineComponent.cs # Content.Shared/Warps/WarpPointSystem.cs # Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs # Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs # Content.Shared/Wieldable/SharedWieldableSystem.cs # Resources/Audio/Ambience/Antag/attributions.yml # Resources/Audio/Ambience/Antag/changeling_start.ogg # Resources/Audio/Effects/attributions.yml # Resources/Audio/Lobby/attributions.yml # Resources/Audio/Machines/attributions.yml # Resources/Audio/Weapons/Guns/Gunshots/attributions.yml # Resources/Locale/en-US/_strings/administration/antag.ftl # Resources/Locale/en-US/_strings/administration/commands/camera.ftl # Resources/Locale/en-US/_strings/administration/commands/dsay-command.ftl # Resources/Locale/en-US/_strings/administration/commands/follow-command.ftl # Resources/Locale/en-US/_strings/administration/smites.ftl # Resources/Locale/en-US/_strings/administration/ui/admin-camera-window.ftl # Resources/Locale/en-US/_strings/administration/ui/role-bans.ftl # Resources/Locale/en-US/_strings/alerts/alerts.ftl # Resources/Locale/en-US/_strings/atmos/gas-pressure-regulator-system.ftl # Resources/Locale/en-US/_strings/bonk/components/bonkable-component.ftl # Resources/Locale/en-US/_strings/commands/add-body-part-command.ftl # Resources/Locale/en-US/_strings/commands/admin-logs-command.ftl # Resources/Locale/en-US/_strings/commands/admin-who-command.ftl # Resources/Locale/en-US/_strings/commands/asay-command.ftl # Resources/Locale/en-US/_strings/commands/crew-manifest-command.ftl # Resources/Locale/en-US/_strings/commands/deadmin-command.ftl # Resources/Locale/en-US/_strings/commands/delete-component-command.ftl # Resources/Locale/en-US/_strings/commands/destroy-mechanism-command.ftl # Resources/Locale/en-US/_strings/commands/dirty-command.ftl # Resources/Locale/en-US/_strings/commands/dsay-command.ftl # Resources/Locale/en-US/_strings/commands/dump-reagent-guide-text-command.ftl # Resources/Locale/en-US/_strings/commands/eye-commands.ftl # Resources/Locale/en-US/_strings/commands/follow-command.ftl # Resources/Locale/en-US/_strings/commands/force-map-command.ftl # Resources/Locale/en-US/_strings/commands/force-preset-command.ftl # Resources/Locale/en-US/_strings/commands/ghost-kick-command.ftl # Resources/Locale/en-US/_strings/commands/ghost-visibility-commands.ftl # Resources/Locale/en-US/_strings/commands/go-lobby-command.ftl # Resources/Locale/en-US/_strings/commands/grid-drag-command.ftl # Resources/Locale/en-US/_strings/commands/is-afk-command.ftl # Resources/Locale/en-US/_strings/commands/makesentient-command.ftl # Resources/Locale/en-US/_strings/commands/map-commands.ftl # Resources/Locale/en-US/_strings/commands/mapping-client-side-setup-command.ftl # Resources/Locale/en-US/_strings/commands/mappingclientsidesetup-command.ftl # Resources/Locale/en-US/_strings/commands/me-command.ftl # Resources/Locale/en-US/_strings/commands/mind-info-command.ftl # Resources/Locale/en-US/_strings/commands/node-vis-commands.ftl # Resources/Locale/en-US/_strings/commands/npc-command.ftl # Resources/Locale/en-US/_strings/commands/nukecodes-command.ftl # Resources/Locale/en-US/_strings/commands/ooc-command.ftl # Resources/Locale/en-US/_strings/commands/pardon-command.ftl # Resources/Locale/en-US/_strings/commands/powerstat-command.ftl # Resources/Locale/en-US/_strings/commands/promote-host-command.ftl # Resources/Locale/en-US/_strings/commands/radiation-command.ftl # Resources/Locale/en-US/_strings/commands/readmin-command.ftl # Resources/Locale/en-US/_strings/commands/remove-extra-components-command.ftl # Resources/Locale/en-US/_strings/commands/role-commands.ftl # Resources/Locale/en-US/_strings/commands/say-command.ftl # Resources/Locale/en-US/_strings/commands/set-battery-percent-command.ftl # Resources/Locale/en-US/_strings/commands/show-ambient-command.ftl # Resources/Locale/en-US/_strings/commands/show-gun-spread-command.ftl # Resources/Locale/en-US/_strings/commands/show-htn-command.ftl # Resources/Locale/en-US/_strings/commands/showrules-command.ftl # Resources/Locale/en-US/_strings/commands/spawn-entity-list-command.ftl # Resources/Locale/en-US/_strings/commands/start-singularity-command.ftl # Resources/Locale/en-US/_strings/commands/tile-pry-command.ftl # Resources/Locale/en-US/_strings/commands/toggle-decals-command.ftl # Resources/Locale/en-US/_strings/commands/toggle-disallow-late-join-command.ftl # Resources/Locale/en-US/_strings/commands/unlocknode-command.ftl # Resources/Locale/en-US/_strings/commands/whisper-command.ftl # Resources/Locale/en-US/_strings/components/gas-pressure-regulator-component.ftl # Resources/Locale/en-US/_strings/engineer-painter/engineer-painter.ftl # Resources/Locale/en-US/_strings/game-ticking/forcemap-command.ftl # Resources/Locale/en-US/_strings/interaction/interaction-popup-component.ftl # Resources/Locale/en-US/_strings/markings/vox_tattoos.ftl # Resources/Locale/en-US/_strings/materials/material-extractor.ftl # Resources/Locale/en-US/_strings/movement/eye.ftl # Resources/Locale/en-US/_strings/nutrition/components/food-component.ftl # Resources/Locale/en-US/_strings/nutrition/components/ingestion-system.ftl # Resources/Locale/en-US/_strings/nutrition/components/sealable-component.ftl # Resources/Locale/en-US/_strings/radiation/radiation-command.ftl # Resources/Locale/en-US/_strings/reagents/meta/physical-desc.ftl # Resources/Locale/en-US/_strings/research/technologies.ftl # Resources/Locale/en-US/_strings/seeds/seeds.ftl # Resources/Locale/en-US/_strings/weapons/grenades/timer-trigger.ftl # Resources/Locale/en-US/_strings/weapons/grenades/voice-trigger.ftl # Resources/Locale/en-US/commands/mapping-client-side-setup-command.ftl # Resources/Locale/en-US/recipes/tags.ftl # Resources/Maps/_Sunrise/Station/bagel.yml # Resources/Maps/_Sunrise/Station/barratry.yml # Resources/Maps/_Sunrise/Station/box.yml # Resources/Maps/_Sunrise/Station/fland.yml # Resources/Maps/_Sunrise/Station/marathon.yml # Resources/Maps/_Sunrise/Station/oasis.yml # Resources/Maps/_Sunrise/Station/packed.yml # Resources/Maps/_Sunrise/Station/plasma.yml # Resources/Maps/amber.yml # Resources/Maps/centcomm.yml # Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml # Resources/Prototypes/Catalog/Fills/Lockers/security.yml # Resources/Prototypes/Catalog/Fills/Lockers/service.yml # Resources/Prototypes/Catalog/Fills/Lockers/suit_storage.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/coffee.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/sustenance.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/winterdrobe.yml # Resources/Prototypes/Entities/Clothing/Belt/belts.yml # Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml # Resources/Prototypes/Entities/Clothing/Eyes/hud.yml # Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml # Resources/Prototypes/Entities/Clothing/Head/hats.yml # Resources/Prototypes/Entities/Clothing/Head/helmets.yml # Resources/Prototypes/Entities/Clothing/Head/hoods.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/softsuits.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml # Resources/Prototypes/Entities/Mobs/NPCs/animals.yml # Resources/Prototypes/Entities/Mobs/NPCs/carp.yml # Resources/Prototypes/Entities/Mobs/NPCs/pets.yml # Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml # Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml # Resources/Prototypes/Entities/Mobs/Player/clone.yml # Resources/Prototypes/Entities/Mobs/Species/base.yml # Resources/Prototypes/Entities/Mobs/Species/dwarf.yml # Resources/Prototypes/Entities/Mobs/base.yml # Resources/Prototypes/Entities/Objects/Fun/pai.yml # Resources/Prototypes/Entities/Objects/Fun/toys.yml # Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml # Resources/Prototypes/Entities/Objects/Shields/shields.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml # Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml # Resources/Prototypes/Entities/Objects/Tools/matches.yml # Resources/Prototypes/Entities/Objects/Weapons/Bombs/plastic.yml # Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/caseless_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/light_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml # Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml # Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml # Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml # Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml # Resources/Prototypes/Entities/Structures/Furniture/beds.yml # Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml # Resources/Prototypes/Entities/Structures/Power/debug_power.yml # Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml # Resources/Prototypes/Entities/Structures/Wallmounts/Misc/mirror.yml # Resources/Prototypes/GameRules/subgamemodes.yml # Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml # Resources/Prototypes/Reagents/Consumable/Drink/soda.yml # Resources/Prototypes/Reagents/medicine.yml # Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml # Resources/Prototypes/Recipes/Lathes/categories.yml # Resources/Prototypes/Recipes/Reactions/chemicals.yml # Resources/Prototypes/Roles/Antags/changeling.yml # Resources/Prototypes/name_identifier_groups.yml # Resources/Prototypes/tool_qualities.yml # Resources/ServerInfo/Guidebook/Antagonist/Zombies.xml # Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml # Resources/ServerInfo/Guidebook/Command.xml # Resources/ServerInfo/Guidebook/Glossary.xml # Resources/ServerInfo/Guidebook/NewPlayer/YourFirstCharacter.xml # Resources/ServerInfo/Guidebook/Science/Robotics.xml # Resources/ServerInfo/Guidebook/Science/ScannersAndVessels.xml # Resources/ServerInfo/Guidebook/Science/Science.xml # Resources/ServerInfo/Guidebook/SpaceStation14.xml # Resources/ServerInfo/Guidebook/Survival.xml # Resources/Textures/Clothing/Head/Hardsuits/atmospherics.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/capspace.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/cybersun.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/engineering-white.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/engineering-white.rsi/on-equipped-HELMET.png # Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/icon.png # Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/on-equipped-HELMET.png # Resources/Textures/Clothing/Head/Hardsuits/medical.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/rd.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/salvage.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/security.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/spatiohelm.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/syndicate.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/syndiecommander.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/syndieelite.rsi/meta.json # Resources/Textures/Clothing/Head/Hardsuits/wizard.rsi/meta.json # Resources/Textures/Clothing/Head/Helmets/atmos_firehelmet.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Armor/captain_carapace.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Armor/security.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/atmospherics.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/capspace.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/cybersun.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/engineering-white.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/engineering.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/medical.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/paramed.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/rd.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/salvage.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/security.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/spatio.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/syndicate.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/syndiecommander.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/syndieelite.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Hardsuits/wizard.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Suits/atmos_firesuit.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Suits/eva.rsi/meta.json # Resources/Textures/Interface/Misc/job_icons.rsi/meta.json # Resources/Textures/Objects/Misc/Lights/lampgreen.rsi/meta.json # Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json # Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json # Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json # Resources/engineCommandPerms.yml # Resources/keybinds.yml # Resources/migration.yml
1008 lines
36 KiB
C#
1008 lines
36 KiB
C#
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Administration.Managers;
|
|
using Content.Client.Chat;
|
|
using Content.Client.Chat.Managers;
|
|
using Content.Client.Chat.TypingIndicator;
|
|
using Content.Client.Chat.UI;
|
|
using Content.Client.Examine;
|
|
using Content.Client.Gameplay;
|
|
using Content.Client.Ghost;
|
|
using Content.Client.Mind;
|
|
using Content.Client.Roles;
|
|
using Content.Client.Stylesheets;
|
|
using Content.Client.UserInterface.Screens;
|
|
using Content.Client.UserInterface.Systems.Chat.Widgets;
|
|
using Content.Client.UserInterface.Systems.Gameplay;
|
|
using Content.Shared._Sunrise.CollectiveMind;
|
|
using Content.Shared.Administration;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Decals;
|
|
using Content.Shared.Damage.ForceSay;
|
|
using Content.Shared.Decals;
|
|
using Content.Shared.Input;
|
|
using Content.Shared.Radio;
|
|
using Content.Shared.Roles.RoleCodeword;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Input;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.State;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controllers;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.GameObjects.Components.Localization;
|
|
using Robust.Shared.Input.Binding;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Replays;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
|
|
namespace Content.Client.UserInterface.Systems.Chat;
|
|
|
|
public sealed partial class ChatUIController : UIController
|
|
{
|
|
[Dependency] private readonly IClientAdminManager _admin = default!;
|
|
[Dependency] private readonly IChatManager _manager = default!;
|
|
[Dependency] private readonly IConfigurationManager _config = default!;
|
|
[Dependency] private readonly IEyeManager _eye = default!;
|
|
[Dependency] private readonly IEntityManager _ent = default!;
|
|
[Dependency] private readonly IInputManager _input = default!;
|
|
[Dependency] private readonly IClientNetManager _net = default!;
|
|
[Dependency] private readonly IPlayerManager _player = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly IStateManager _state = default!;
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
|
|
|
[UISystemDependency] private readonly ExamineSystem? _examine = default;
|
|
[UISystemDependency] private readonly GhostSystem? _ghost = default;
|
|
[UISystemDependency] private readonly CollectiveMindChatUpdateSystem? _collectiveMind = default!;
|
|
[UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default;
|
|
[UISystemDependency] private readonly ChatSystem? _chatSys = default;
|
|
[UISystemDependency] private readonly TransformSystem? _transform = default;
|
|
[UISystemDependency] private readonly MindSystem? _mindSystem = default!;
|
|
[UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!;
|
|
|
|
private static readonly ProtoId<ColorPalettePrototype> ChatNamePalette = "ChatNames";
|
|
private string[] _chatNameColors = default!;
|
|
private bool _chatNameColorsEnabled;
|
|
|
|
private ISawmill _sawmill = default!;
|
|
|
|
public static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
|
|
{
|
|
{SharedChatSystem.LocalPrefix, ChatSelectChannel.Local},
|
|
{SharedChatSystem.WhisperPrefix, ChatSelectChannel.Whisper},
|
|
{SharedChatSystem.ConsolePrefix, ChatSelectChannel.Console},
|
|
{SharedChatSystem.LOOCPrefix, ChatSelectChannel.LOOC},
|
|
{SharedChatSystem.OOCPrefix, ChatSelectChannel.OOC},
|
|
{SharedChatSystem.EmotesPrefix, ChatSelectChannel.Emotes},
|
|
{SharedChatSystem.EmotesAltPrefix, ChatSelectChannel.Emotes},
|
|
{SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin},
|
|
{SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio},
|
|
{SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead},
|
|
{SharedChatSystem.CollectiveMindPrefix, ChatSelectChannel.CollectiveMind} // Sunrise-Edit
|
|
};
|
|
|
|
public static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes = new()
|
|
{
|
|
{ChatSelectChannel.Local, SharedChatSystem.LocalPrefix},
|
|
{ChatSelectChannel.Whisper, SharedChatSystem.WhisperPrefix},
|
|
{ChatSelectChannel.Console, SharedChatSystem.ConsolePrefix},
|
|
{ChatSelectChannel.LOOC, SharedChatSystem.LOOCPrefix},
|
|
{ChatSelectChannel.OOC, SharedChatSystem.OOCPrefix},
|
|
{ChatSelectChannel.Emotes, SharedChatSystem.EmotesPrefix},
|
|
{ChatSelectChannel.Admin, SharedChatSystem.AdminPrefix},
|
|
{ChatSelectChannel.Radio, SharedChatSystem.RadioCommonPrefix},
|
|
{ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix},
|
|
{ChatSelectChannel.CollectiveMind, SharedChatSystem.CollectiveMindPrefix} // Sunrise-Edit
|
|
};
|
|
|
|
/// <summary>
|
|
/// The max amount of chars allowed to fit in a single speech bubble.
|
|
/// </summary>
|
|
private const int SingleBubbleCharLimit = 100;
|
|
|
|
/// <summary>
|
|
/// Base queue delay each speech bubble has.
|
|
/// </summary>
|
|
private const float BubbleDelayBase = 0.2f;
|
|
|
|
/// <summary>
|
|
/// Factor multiplied by speech bubble char length to add to delay.
|
|
/// </summary>
|
|
private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit;
|
|
|
|
/// <summary>
|
|
/// The max amount of speech bubbles over a single entity at once.
|
|
/// </summary>
|
|
private const int SpeechBubbleCap = 4;
|
|
|
|
private LayoutContainer _speechBubbleRoot = default!;
|
|
|
|
/// <summary>
|
|
/// Speech bubbles that are currently visible on screen.
|
|
/// We track them to push them up when new ones get added.
|
|
/// </summary>
|
|
private readonly Dictionary<EntityUid, List<SpeechBubble>> _activeSpeechBubbles =
|
|
new();
|
|
|
|
/// <summary>
|
|
/// Speech bubbles that are to-be-sent because of the "rate limit" they have.
|
|
/// </summary>
|
|
private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
|
|
= new();
|
|
|
|
private readonly HashSet<ChatBox> _chats = new();
|
|
public IReadOnlySet<ChatBox> Chats => _chats;
|
|
|
|
/// <summary>
|
|
/// The max amount of characters an entity can send in one message
|
|
/// </summary>
|
|
public int MaxMessageLength => _config.GetCVar(CCVars.ChatMaxMessageLength);
|
|
|
|
/// <summary>
|
|
/// For currently disabled chat filters,
|
|
/// unread messages (messages received since the channel has been filtered out).
|
|
/// </summary>
|
|
private readonly Dictionary<ChatChannel, int> _unreadMessages = new();
|
|
|
|
// TODO add a cap for this for non-replays
|
|
public readonly List<(GameTick Tick, ChatMessage Msg)> History = new();
|
|
|
|
// Maintains which channels a client should be able to filter (for showing in the chatbox)
|
|
// and select (for attempting to send on).
|
|
// This may not always actually match with what the server will actually allow them to
|
|
// send / receive on, it is only what the user can select in the UI. For example,
|
|
// if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
|
|
// to the server to handle invalid attempts to use particular channels and not send messages for
|
|
// channels the user shouldn't be able to hear.
|
|
//
|
|
// Note that Command is an available selection in the chatbox channel selector,
|
|
// which is not actually a chat channel but is always available.
|
|
public ChatSelectChannel CanSendChannels { get; private set; }
|
|
public ChatChannel FilterableChannels { get; private set; }
|
|
public ChatSelectChannel SelectableChannels { get; private set; }
|
|
private ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
|
|
|
|
public event Action<ChatSelectChannel>? CanSendChannelsChanged;
|
|
public event Action<ChatChannel>? FilterableChannelsChanged;
|
|
public event Action<ChatSelectChannel>? SelectableChannelsChanged;
|
|
public event Action<ChatChannel, int?>? UnreadMessageCountsUpdated;
|
|
public event Action<ChatMessage>? MessageAdded;
|
|
|
|
public override void Initialize()
|
|
{
|
|
_sawmill = Logger.GetSawmill("chat");
|
|
_sawmill.Level = LogLevel.Info;
|
|
_admin.AdminStatusUpdated += UpdateChannelPermissions;
|
|
_manager.PermissionsUpdated += UpdateChannelPermissions; // Sunrise-Edit
|
|
_player.LocalPlayerAttached += OnAttachedChanged;
|
|
_player.LocalPlayerDetached += OnAttachedChanged;
|
|
_state.OnStateChanged += StateChanged;
|
|
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
|
_net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
|
|
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
|
|
_config.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
|
|
_chatNameColorsEnabled = _config.GetCVar(CCVars.ChatEnableColorName);
|
|
|
|
_speechBubbleRoot = new LayoutContainer();
|
|
|
|
UpdateChannelPermissions();
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChat()));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusLocalChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Local)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusEmote,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Emotes)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusWhisperChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Whisper)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusLOOC,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.LOOC)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.OOC)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Admin)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusRadio,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Radio)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusDeadChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Dead)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusConsoleChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Console)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
|
|
InputCmdHandler.FromDelegate(_ => CycleChatChannel(true)));
|
|
|
|
_input.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
|
|
InputCmdHandler.FromDelegate(_ => CycleChatChannel(false)));
|
|
|
|
// Sunrise-Start
|
|
_input.SetInputCommand(ContentKeyFunctions.FocusCollectiveMindChat,
|
|
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.CollectiveMind)));
|
|
// Sunrise-End
|
|
|
|
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
|
|
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
|
|
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
|
|
|
|
var nameColors = _prototypeManager.Index(ChatNamePalette).Colors.Values.ToArray();
|
|
_chatNameColors = new string[nameColors.Length];
|
|
for (var i = 0; i < nameColors.Length; i++)
|
|
{
|
|
_chatNameColors[i] = nameColors[i].ToHex();
|
|
}
|
|
|
|
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
|
|
|
|
InitializeHighlights();
|
|
}
|
|
|
|
public void OnScreenLoad()
|
|
{
|
|
SetMainChat(true);
|
|
|
|
var viewportContainer = UIManager.ActiveScreen!.FindControl<LayoutContainer>("ViewportContainer");
|
|
SetSpeechBubbleRoot(viewportContainer);
|
|
|
|
SetChatWindowOpacity(_config.GetCVar(CCVars.ChatWindowOpacity));
|
|
}
|
|
|
|
public void OnScreenUnload()
|
|
{
|
|
SetMainChat(false);
|
|
}
|
|
|
|
private void OnChatWindowOpacityChanged(float opacity)
|
|
{
|
|
SetChatWindowOpacity(opacity);
|
|
}
|
|
|
|
public void SetChatWindowOpacity(float opacity)
|
|
{
|
|
var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
|
|
|
|
var panel = chatBox?.ChatWindowPanel;
|
|
if (panel is null)
|
|
return;
|
|
|
|
Color color;
|
|
if (panel.PanelOverride is StyleBoxFlat styleBoxFlat)
|
|
color = styleBoxFlat.BackgroundColor;
|
|
else if (panel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var style)
|
|
&& style is StyleBoxFlat propStyleBoxFlat)
|
|
color = propStyleBoxFlat.BackgroundColor;
|
|
else
|
|
color = StyleNano.ChatBackgroundColor;
|
|
|
|
panel.PanelOverride = new StyleBoxFlat
|
|
{
|
|
BackgroundColor = color.WithAlpha(opacity)
|
|
};
|
|
}
|
|
|
|
public void SetMainChat(bool setting)
|
|
{
|
|
if (UIManager.ActiveScreen == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChatBox chatBox;
|
|
string? chatSizeRaw;
|
|
|
|
switch (UIManager.ActiveScreen)
|
|
{
|
|
case DefaultGameScreen defaultScreen:
|
|
chatBox = defaultScreen.ChatBox;
|
|
chatSizeRaw = _config.GetCVar(CCVars.DefaultScreenChatSize);
|
|
SetChatSizing(chatSizeRaw, defaultScreen, setting);
|
|
break;
|
|
case SeparatedChatGameScreen separatedScreen:
|
|
chatBox = separatedScreen.ChatBox;
|
|
chatSizeRaw = _config.GetCVar(CCVars.SeparatedScreenChatSize);
|
|
SetChatSizing(chatSizeRaw, separatedScreen, setting);
|
|
break;
|
|
default:
|
|
// this could be better?
|
|
var maybeChat = UIManager.ActiveScreen.GetWidget<ChatBox>();
|
|
|
|
chatBox = maybeChat ?? throw new Exception("Cannot get chat box in screen!");
|
|
|
|
break;
|
|
}
|
|
|
|
chatBox.Main = setting;
|
|
}
|
|
|
|
private void SetChatSizing(string sizing, InGameScreen screen, bool setting)
|
|
{
|
|
if (!setting)
|
|
{
|
|
screen.OnChatResized -= StoreChatSize;
|
|
return;
|
|
}
|
|
|
|
screen.OnChatResized += StoreChatSize;
|
|
|
|
if (string.IsNullOrEmpty(sizing))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var split = sizing.Split(",");
|
|
|
|
var chatSize = new Vector2(
|
|
float.Parse(split[0], CultureInfo.InvariantCulture),
|
|
float.Parse(split[1], CultureInfo.InvariantCulture));
|
|
|
|
|
|
screen.SetChatSize(chatSize);
|
|
}
|
|
|
|
private void StoreChatSize(Vector2 size)
|
|
{
|
|
if (UIManager.ActiveScreen == null)
|
|
{
|
|
throw new Exception("Cannot get active screen!");
|
|
}
|
|
|
|
var stringSize =
|
|
$"{size.X.ToString(CultureInfo.InvariantCulture)},{size.Y.ToString(CultureInfo.InvariantCulture)}";
|
|
switch (UIManager.ActiveScreen)
|
|
{
|
|
case DefaultGameScreen _:
|
|
_config.SetCVar(CCVars.DefaultScreenChatSize, stringSize);
|
|
break;
|
|
case SeparatedChatGameScreen _:
|
|
_config.SetCVar(CCVars.SeparatedScreenChatSize, stringSize);
|
|
break;
|
|
default:
|
|
// do nothing
|
|
return;
|
|
}
|
|
|
|
_config.SaveToFile();
|
|
}
|
|
|
|
private void FocusChat()
|
|
{
|
|
foreach (var chat in _chats)
|
|
{
|
|
if (!chat.Main)
|
|
continue;
|
|
|
|
chat.Focus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void FocusChannel(ChatSelectChannel channel)
|
|
{
|
|
foreach (var chat in _chats)
|
|
{
|
|
if (!chat.Main)
|
|
continue;
|
|
|
|
chat.Focus(channel);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void CycleChatChannel(bool forward)
|
|
{
|
|
foreach (var chat in _chats)
|
|
{
|
|
if (!chat.Main)
|
|
continue;
|
|
|
|
chat.CycleChatChannel(forward);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void StateChanged(StateChangedEventArgs args)
|
|
{
|
|
if (args.NewState is GameplayState)
|
|
{
|
|
PreferredChannel = ChatSelectChannel.Local;
|
|
}
|
|
|
|
UpdateChannelPermissions();
|
|
}
|
|
|
|
public void SetSpeechBubbleRoot(LayoutContainer root)
|
|
{
|
|
_speechBubbleRoot.Orphan();
|
|
root.AddChild(_speechBubbleRoot);
|
|
LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide);
|
|
_speechBubbleRoot.SetPositionLast();
|
|
}
|
|
|
|
private void OnAttachedChanged(EntityUid uid)
|
|
{
|
|
UpdateChannelPermissions();
|
|
|
|
UpdateAutoFillHighlights();
|
|
}
|
|
|
|
private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
|
|
{
|
|
var ent = EntityManager.GetEntity(msg.SenderEntity);
|
|
|
|
if (!EntityManager.EntityExists(ent))
|
|
{
|
|
_sawmill.Debug("Got local chat message with invalid sender entity: {0}", msg.SenderEntity);
|
|
return;
|
|
}
|
|
|
|
EnqueueSpeechBubble(ent, msg, speechType);
|
|
}
|
|
|
|
private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData)
|
|
{
|
|
var bubble =
|
|
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity);
|
|
|
|
bubble.OnDied += SpeechBubbleDied;
|
|
|
|
if (_activeSpeechBubbles.TryGetValue(entity, out var existing))
|
|
{
|
|
// Push up existing bubbles above the mob's head.
|
|
foreach (var existingBubble in existing)
|
|
{
|
|
existingBubble.VerticalOffset += bubble.ContentSize.Y;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
existing = new List<SpeechBubble>();
|
|
_activeSpeechBubbles.Add(entity, existing);
|
|
}
|
|
|
|
existing.Add(bubble);
|
|
_speechBubbleRoot.AddChild(bubble);
|
|
|
|
if (existing.Count > SpeechBubbleCap)
|
|
{
|
|
// Get the next speech bubble to fade
|
|
// Any speech bubbles before it are already fading
|
|
var last = existing[^(SpeechBubbleCap + 1)];
|
|
last.FadeNow();
|
|
}
|
|
}
|
|
|
|
private void SpeechBubbleDied(EntityUid entity, SpeechBubble bubble)
|
|
{
|
|
RemoveSpeechBubble(entity, bubble);
|
|
}
|
|
|
|
private void EnqueueSpeechBubble(EntityUid entity, ChatMessage message, SpeechBubble.SpeechType speechType)
|
|
{
|
|
// Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
|
|
if (EntityManager.GetComponent<TransformComponent>(entity).MapID != _eye.CurrentEye.Position.MapId)
|
|
return;
|
|
|
|
if (!_queuedSpeechBubbles.TryGetValue(entity, out var queueData))
|
|
{
|
|
queueData = new SpeechBubbleQueueData();
|
|
_queuedSpeechBubbles.Add(entity, queueData);
|
|
}
|
|
|
|
queueData.MessageQueue.Enqueue(new SpeechBubbleData(message, speechType));
|
|
}
|
|
|
|
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
|
{
|
|
bubble.Dispose();
|
|
|
|
var list = _activeSpeechBubbles[entityUid];
|
|
list.Remove(bubble);
|
|
|
|
if (list.Count == 0)
|
|
{
|
|
_activeSpeechBubbles.Remove(entityUid);
|
|
}
|
|
}
|
|
|
|
private void UpdateChannelPermissions()
|
|
{
|
|
CanSendChannels = default;
|
|
FilterableChannels = default;
|
|
|
|
// Can always send console stuff.
|
|
CanSendChannels |= ChatSelectChannel.Console;
|
|
|
|
// can always send/recieve OOC
|
|
CanSendChannels |= ChatSelectChannel.OOC;
|
|
CanSendChannels |= ChatSelectChannel.LOOC;
|
|
FilterableChannels |= ChatChannel.OOC;
|
|
FilterableChannels |= ChatChannel.LOOC;
|
|
|
|
// can always hear server (nobody can actually send server messages).
|
|
FilterableChannels |= ChatChannel.Server;
|
|
|
|
if (_state.CurrentState is GameplayStateBase)
|
|
{
|
|
// can always hear local / radio / emote / notifications when in the game
|
|
FilterableChannels |= ChatChannel.Local;
|
|
FilterableChannels |= ChatChannel.Whisper;
|
|
FilterableChannels |= ChatChannel.Radio;
|
|
FilterableChannels |= ChatChannel.Emotes;
|
|
FilterableChannels |= ChatChannel.Notifications;
|
|
|
|
// Can only send local / radio / emote when attached to a non-ghost entity.
|
|
// TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
|
|
if (_ghost is not {IsGhost: true})
|
|
{
|
|
CanSendChannels |= ChatSelectChannel.Local;
|
|
CanSendChannels |= ChatSelectChannel.Whisper;
|
|
CanSendChannels |= ChatSelectChannel.Radio;
|
|
CanSendChannels |= ChatSelectChannel.Emotes;
|
|
}
|
|
}
|
|
|
|
// Only ghosts and admins can send / see deadchat.
|
|
if (_admin.HasFlag(AdminFlags.Admin) || _ghost is {IsGhost: true})
|
|
{
|
|
FilterableChannels |= ChatChannel.Dead;
|
|
CanSendChannels |= ChatSelectChannel.Dead;
|
|
}
|
|
|
|
// only admins can see / filter asay
|
|
if (_admin.HasFlag(AdminFlags.Adminchat))
|
|
{
|
|
FilterableChannels |= ChatChannel.Admin;
|
|
FilterableChannels |= ChatChannel.AdminAlert;
|
|
FilterableChannels |= ChatChannel.AdminChat;
|
|
CanSendChannels |= ChatSelectChannel.Admin;
|
|
FilterableChannels |= ChatChannel.CollectiveMind; // Sunrise-Edit
|
|
}
|
|
|
|
// Sunrise-Start
|
|
if (_collectiveMind != null && _collectiveMind.IsCollectiveMind)
|
|
{
|
|
FilterableChannels |= ChatChannel.CollectiveMind;
|
|
CanSendChannels |= ChatSelectChannel.CollectiveMind;
|
|
}
|
|
// Sunrise-End
|
|
|
|
SelectableChannels = CanSendChannels;
|
|
|
|
// Necessary so that we always have a channel to fall back to.
|
|
DebugTools.Assert((CanSendChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
|
|
DebugTools.Assert((FilterableChannels & ChatChannel.OOC) != 0, "OOC must always be available");
|
|
DebugTools.Assert((SelectableChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
|
|
|
|
// let our chatbox know all the new settings
|
|
CanSendChannelsChanged?.Invoke(CanSendChannels);
|
|
FilterableChannelsChanged?.Invoke(FilterableChannels);
|
|
SelectableChannelsChanged?.Invoke(SelectableChannels);
|
|
}
|
|
|
|
public void ClearUnfilteredUnreads(ChatChannel channels)
|
|
{
|
|
foreach (var channel in _unreadMessages.Keys.ToArray())
|
|
{
|
|
if ((channels & channel) == 0)
|
|
continue;
|
|
|
|
_unreadMessages[channel] = 0;
|
|
UnreadMessageCountsUpdated?.Invoke(channel, 0);
|
|
}
|
|
}
|
|
|
|
public override void FrameUpdate(FrameEventArgs delta)
|
|
{
|
|
UpdateQueuedSpeechBubbles(delta);
|
|
}
|
|
|
|
private void UpdateQueuedSpeechBubbles(FrameEventArgs delta)
|
|
{
|
|
// Update queued speech bubbles.
|
|
if (_queuedSpeechBubbles.Count == 0 || _examine == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var (entity, queueData) in _queuedSpeechBubbles.ShallowClone())
|
|
{
|
|
if (!EntityManager.EntityExists(entity))
|
|
{
|
|
_queuedSpeechBubbles.Remove(entity);
|
|
continue;
|
|
}
|
|
|
|
queueData.TimeLeft -= delta.DeltaSeconds;
|
|
if (queueData.TimeLeft > 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (queueData.MessageQueue.Count == 0)
|
|
{
|
|
_queuedSpeechBubbles.Remove(entity);
|
|
continue;
|
|
}
|
|
|
|
var msg = queueData.MessageQueue.Dequeue();
|
|
|
|
queueData.TimeLeft += BubbleDelayBase + msg.Message.Message.Length * BubbleDelayFactor;
|
|
|
|
// We keep the queue around while it has 0 items. This allows us to keep the timer.
|
|
// When the timer hits 0 and there's no messages left, THEN we can clear it up.
|
|
CreateSpeechBubble(entity, msg);
|
|
}
|
|
|
|
var player = _player.LocalEntity;
|
|
var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
|
|
=> uid == data.compOwner || uid == data.attachedEntity;
|
|
var playerPos = player != null
|
|
? _eye.CurrentEye.Position
|
|
: MapCoordinates.Nullspace;
|
|
|
|
var occluded = player != null && _examine.IsOccluded(player.Value);
|
|
|
|
foreach (var (ent, bubs) in _activeSpeechBubbles)
|
|
{
|
|
if (EntityManager.Deleted(ent))
|
|
{
|
|
SetBubbles(bubs, false);
|
|
continue;
|
|
}
|
|
|
|
if (ent == player)
|
|
{
|
|
SetBubbles(bubs, true);
|
|
continue;
|
|
}
|
|
|
|
var otherPos = _transform?.GetMapCoordinates(ent) ?? MapCoordinates.Nullspace;
|
|
|
|
if (occluded && !_examine.InRangeUnOccluded(
|
|
playerPos,
|
|
otherPos, 0f,
|
|
(ent, player), predicate))
|
|
{
|
|
SetBubbles(bubs, false);
|
|
continue;
|
|
}
|
|
|
|
SetBubbles(bubs, true);
|
|
}
|
|
}
|
|
|
|
private void SetBubbles(List<SpeechBubble> bubbles, bool visible)
|
|
{
|
|
foreach (var bubble in bubbles)
|
|
{
|
|
bubble.Visible = visible;
|
|
}
|
|
}
|
|
|
|
public ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel)
|
|
{
|
|
if (channel == ChatSelectChannel.Local && _ghost is {IsGhost: true})
|
|
return ChatSelectChannel.Dead;
|
|
|
|
return channel;
|
|
}
|
|
|
|
private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioChannel)
|
|
{
|
|
radioChannel = null;
|
|
return _player.LocalEntity is EntityUid { Valid: true } uid
|
|
&& _chatSys != null
|
|
&& _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
|
|
}
|
|
|
|
// Sunrise-Start
|
|
private bool TryGetCollectiveMind(string text, out CollectiveMindPrototype? collectiveMind)
|
|
{
|
|
collectiveMind = null;
|
|
return _player.LocalEntity is { Valid: true } uid
|
|
&& _chatSys != null
|
|
&& _chatSys.TryProccessCollectiveMindMessage(uid, text, out _, out collectiveMind, quiet: true);
|
|
}
|
|
|
|
public void UpdateSelectedChannel(ChatBox box)
|
|
{
|
|
var (prefixChannel, _, radioChannel, collectiveMind) = SplitInputContents(box.ChatInput.Input.Text.ToLower());
|
|
|
|
switch (prefixChannel)
|
|
{
|
|
case ChatSelectChannel.None:
|
|
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null, null);
|
|
break;
|
|
case ChatSelectChannel.CollectiveMind:
|
|
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, null, collectiveMind);
|
|
break;
|
|
default:
|
|
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel, null);
|
|
break;
|
|
}
|
|
}
|
|
// Sunrise-End
|
|
|
|
public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel, CollectiveMindPrototype? collectiveMind) SplitInputContents(string text)
|
|
{
|
|
text = text.Trim();
|
|
if (text.Length == 0)
|
|
return (ChatSelectChannel.None, text, null, null); // Sunrise-Edit
|
|
|
|
// We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command
|
|
// because ????????
|
|
|
|
ChatSelectChannel chatChannel;
|
|
if (TryGetRadioChannel(text, out var radioChannel))
|
|
chatChannel = ChatSelectChannel.Radio;
|
|
else
|
|
chatChannel = PrefixToChannel.GetValueOrDefault(text[0]);
|
|
|
|
if ((CanSendChannels & chatChannel) == 0)
|
|
return (ChatSelectChannel.None, text, null, null); // Sunrise-Edit
|
|
|
|
if (chatChannel == ChatSelectChannel.Radio)
|
|
return (chatChannel, text, radioChannel, null); // Sunrise-Edit
|
|
|
|
// Sunrise-Start
|
|
if (TryGetCollectiveMind(text, out var collectiveMind) && chatChannel == ChatSelectChannel.CollectiveMind)
|
|
return (chatChannel, text, radioChannel, collectiveMind);
|
|
// Sunrise-End
|
|
|
|
if (chatChannel == ChatSelectChannel.Local)
|
|
{
|
|
if (_ghost?.IsGhost != true)
|
|
return (chatChannel, text, null, null); // Sunrise-Edit
|
|
else
|
|
chatChannel = ChatSelectChannel.Dead;
|
|
}
|
|
|
|
return (chatChannel, text[1..].TrimStart(), null, null); // Sunrise-Edit
|
|
}
|
|
|
|
public void SendMessage(ChatBox box, ChatSelectChannel channel)
|
|
{
|
|
_typingIndicator?.ClientSubmittedChatText();
|
|
|
|
var text = box.ChatInput.Input.Text;
|
|
box.ChatInput.Input.Clear();
|
|
box.ChatInput.Input.ReleaseKeyboardFocus();
|
|
UpdateSelectedChannel(box);
|
|
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return;
|
|
|
|
(var prefixChannel, text, var _, var _) = SplitInputContents(text); // Sunrise-Edit
|
|
|
|
// Check if message is longer than the character limit
|
|
if (text.Length > MaxMessageLength)
|
|
{
|
|
var locWarning = Loc.GetString("chat-manager-max-message-length",
|
|
("maxMessageLength", MaxMessageLength));
|
|
box.AddLine(locWarning, Color.Orange);
|
|
return;
|
|
}
|
|
|
|
if (prefixChannel != ChatSelectChannel.None)
|
|
channel = prefixChannel;
|
|
else if (channel == ChatSelectChannel.Radio)
|
|
{
|
|
// radio must have prefix as it goes through the say command.
|
|
text = $";{text}";
|
|
}
|
|
|
|
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
|
}
|
|
|
|
private void OnDamageForceSay(DamageForceSayEvent ev, EntitySessionEventArgs _)
|
|
{
|
|
var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
|
|
if (chatBox == null)
|
|
return;
|
|
|
|
var msg = chatBox.ChatInput.Input.Text.TrimEnd();
|
|
// Don't send on OOC/LOOC obviously!
|
|
|
|
// we need to handle selected channel
|
|
// and prefix-channel separately..
|
|
var allowedChannels = ChatSelectChannel.Local | ChatSelectChannel.Whisper;
|
|
if ((chatBox.SelectedChannel & allowedChannels) == ChatSelectChannel.None)
|
|
return;
|
|
|
|
// none can be returned from this if theres no prefix,
|
|
// so we allow it in that case (assuming the previous check will have exited already if its an invalid channel)
|
|
var prefixChannel = SplitInputContents(msg).chatChannel;
|
|
if (prefixChannel != ChatSelectChannel.None && (prefixChannel & allowedChannels) == ChatSelectChannel.None)
|
|
return;
|
|
|
|
if (_player.LocalSession?.AttachedEntity is not { } ent
|
|
|| !EntityManager.TryGetComponent<DamageForceSayComponent>(ent, out var forceSay))
|
|
return;
|
|
|
|
if (string.IsNullOrWhiteSpace(msg))
|
|
return;
|
|
|
|
var modifiedText = ev.Suffix != null
|
|
? Loc.GetString(forceSay.ForceSayMessageWrap,
|
|
("message", msg), ("suffix", ev.Suffix))
|
|
: Loc.GetString(forceSay.ForceSayMessageWrapNoSuffix,
|
|
("message", msg));
|
|
|
|
chatBox.ChatInput.Input.SetText(modifiedText);
|
|
chatBox.ChatInput.Input.ForceSubmitText();
|
|
}
|
|
|
|
private void OnChatMessage(MsgChatMessage message)
|
|
{
|
|
var msg = message.Message;
|
|
ProcessChatMessage(msg);
|
|
|
|
if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
|
|
_config.GetCVar(CCVars.ReplayRecordAdminChat))
|
|
{
|
|
_replayRecording.RecordClientMessage(msg);
|
|
}
|
|
}
|
|
|
|
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
|
|
{
|
|
// color the name unless it's something like "the old man"
|
|
if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Whisper) && _chatNameColorsEnabled)
|
|
{
|
|
var grammar = _ent.GetComponentOrNull<GrammarComponent>(_ent.GetEntity(msg.SenderEntity));
|
|
if (grammar != null && grammar.ProperNoun == true)
|
|
msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name")));
|
|
}
|
|
|
|
// Color any words chosen by the client.
|
|
foreach (var highlight in _highlights)
|
|
{
|
|
msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor);
|
|
}
|
|
|
|
// Color any codewords for minds that have roles that use them
|
|
if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null)
|
|
{
|
|
if (_mindSystem.TryGetMind(_player.LocalUser.Value, out var mindId) && _ent.TryGetComponent(mindId, out RoleCodewordComponent? codewordComp))
|
|
{
|
|
foreach (var (_, codewordData) in codewordComp.RoleCodewords)
|
|
{
|
|
foreach (string codeword in codewordData.Codewords)
|
|
msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, codeword, "color", codewordData.Color.ToHex());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log all incoming chat to repopulate when filter is un-toggled
|
|
if (!msg.HideChat)
|
|
{
|
|
History.Add((_timing.CurTick, msg));
|
|
MessageAdded?.Invoke(msg);
|
|
|
|
if (!msg.Read)
|
|
{
|
|
_sawmill.Debug($"Message filtered: {msg.Channel}: {msg.Message}");
|
|
if (!_unreadMessages.TryGetValue(msg.Channel, out var count))
|
|
count = 0;
|
|
|
|
count += 1;
|
|
_unreadMessages[msg.Channel] = count;
|
|
UnreadMessageCountsUpdated?.Invoke(msg.Channel, count);
|
|
}
|
|
}
|
|
|
|
// Local messages that have an entity attached get a speech bubble.
|
|
if (!speechBubble || msg.SenderEntity == default)
|
|
return;
|
|
|
|
switch (msg.Channel)
|
|
{
|
|
case ChatChannel.Local:
|
|
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
|
break;
|
|
|
|
case ChatChannel.Whisper:
|
|
AddSpeechBubble(msg, SpeechBubble.SpeechType.Whisper);
|
|
break;
|
|
|
|
case ChatChannel.Dead:
|
|
if (_ghost is not {IsGhost: true})
|
|
break;
|
|
|
|
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
|
break;
|
|
|
|
case ChatChannel.Emotes:
|
|
AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote);
|
|
break;
|
|
|
|
case ChatChannel.LOOC:
|
|
if (_config.GetCVar(CCVars.LoocAboveHeadShow))
|
|
AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void OnDeleteChatMessagesBy(MsgDeleteChatMessagesBy msg)
|
|
{
|
|
// This will delete messages from an entity even if different players were the author.
|
|
// Usages of the erase admin verb should be rare enough that this does not matter.
|
|
// Otherwise the client would need to know that one entity has multiple author players,
|
|
// or the server would need to track when and which entities a player sent messages as.
|
|
History.RemoveAll(h => h.Msg.SenderKey == msg.Key || msg.Entities.Contains(h.Msg.SenderEntity));
|
|
Repopulate();
|
|
}
|
|
|
|
public void RegisterChat(ChatBox chat)
|
|
{
|
|
_chats.Add(chat);
|
|
}
|
|
|
|
public void UnregisterChat(ChatBox chat)
|
|
{
|
|
_chats.Remove(chat);
|
|
}
|
|
|
|
public ChatSelectChannel GetPreferredChannel()
|
|
{
|
|
return MapLocalIfGhost(PreferredChannel);
|
|
}
|
|
|
|
public void NotifyChatTextChange()
|
|
{
|
|
_typingIndicator?.ClientChangedChatText();
|
|
}
|
|
|
|
public void NotifyChatFocus(bool isFocused)
|
|
{
|
|
_typingIndicator?.ClientChangedChatFocus(isFocused);
|
|
}
|
|
|
|
public void Repopulate()
|
|
{
|
|
foreach (var chat in _chats)
|
|
{
|
|
chat.Repopulate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the chat name color for a mob
|
|
/// </summary>
|
|
/// <param name="name">Name of the mob</param>
|
|
/// <returns>Hex value of the color</returns>
|
|
public string GetNameColor(string name)
|
|
{
|
|
var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length);
|
|
return _chatNameColors[colorIdx];
|
|
}
|
|
|
|
private readonly record struct SpeechBubbleData(ChatMessage Message, SpeechBubble.SpeechType Type);
|
|
|
|
private sealed class SpeechBubbleQueueData
|
|
{
|
|
/// <summary>
|
|
/// Time left until the next speech bubble can appear.
|
|
/// </summary>
|
|
public float TimeLeft { get; set; }
|
|
|
|
public Queue<SpeechBubbleData> MessageQueue { get; } = new();
|
|
}
|
|
}
|