Skip to content
Draft
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0c74252
Nuke BwoinkSystem.cs
Simyon264 Oct 12, 2025
9bcdb38
WIP: Ahelp refactor
Simyon264 Oct 13, 2025
4cebe7c
Remove leftover AHelpMessage
Simyon264 Oct 13, 2025
0786290
Add doc-comment to Flags
Simyon264 Oct 14, 2025
6388982
Make message flags a byte
Simyon264 Oct 14, 2025
fec6130
Fix lobby button going out of sync if you close window manually
Simyon264 Oct 14, 2025
048194b
Clear text output when syncing lines
Simyon264 Oct 14, 2025
7fba8cf
add channel help text when clearing once again
Simyon264 Oct 14, 2025
71aadeb
Remove unused method for now
Simyon264 Oct 14, 2025
4a323d3
implement message sync
Simyon264 Oct 14, 2025
62ece7c
fix admins sending in their own channel resulting in no messages bein…
Simyon264 Oct 14, 2025
91772cd
don't send help text for manager panels
Simyon264 Oct 14, 2025
bbf28ab
properly handle rich text
Simyon264 Oct 14, 2025
a2edef7
convert into method group
Simyon264 Oct 14, 2025
5d2c31e
Make unreads work properly
Simyon264 Oct 14, 2025
e670e93
minor, make props readonly
Simyon264 Oct 14, 2025
c62b8c4
Fix some issues with UI lifetime (i think)
Simyon264 Oct 14, 2025
7ab24a5
Fix unclosed menu not keeping unread count
Simyon264 Oct 14, 2025
7680a2d
also do this for last message
Simyon264 Oct 14, 2025
9325c39
give nik the sound they deserve
Simyon264 Oct 14, 2025
fb9132b
ui manager isn't real
Simyon264 Oct 14, 2025
384f689
add support for channels without sound play
Simyon264 Oct 19, 2025
ad8d7bc
remove unused imports
Simyon264 Oct 19, 2025
9ffd69a
Register a shared instance for bwoink manager
Simyon264 Oct 19, 2025
deb6cff
there IS a better way of doing this
Simyon264 Oct 19, 2025
f777c84
it did, in fact, not work
Simyon264 Oct 19, 2025
3cb4c53
i have, the stupid
Simyon264 Oct 19, 2025
26ae3fd
i have the stupid disease
Simyon264 Oct 19, 2025
62f5457
implement typing status
Simyon264 Oct 19, 2025
5130470
Merge branch 'master' into bwoink-system-refactor
Simyon264 Oct 19, 2025
65b3e3c
fix conflicts
Simyon264 Oct 19, 2025
bfa77e9
cleanup and add more comments
Simyon264 Oct 19, 2025
65eda47
Add public API and toolshed command for bwoinking
Simyon264 Oct 20, 2025
1149532
properly switch to channel when getting bwoinked in that channel
Simyon264 Oct 20, 2025
b845dbd
add temporary testing channel
Simyon264 Oct 20, 2025
bd8c40f
half working implementation of better requirements for channels
Simyon264 Oct 20, 2025
ee2858e
finish list requirement and handling channel syncs
Simyon264 Oct 20, 2025
3c05c5e
Implement OperationMode.None
Simyon264 Oct 20, 2025
6bfc5c7
add sync command
Simyon264 Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions Content.Client/Administration/Managers/ClientBwoinkManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using System.Linq;
using Content.Shared.Administration.Managers.Bwoink;
using Content.Shared.Administration.Managers.Bwoink.Features;
using Robust.Client.Audio;
using Robust.Client.ResourceManagement;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;

namespace Content.Client.Administration.Managers;

public sealed class ClientBwoinkManager : SharedBwoinkManager
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IAudioManager _audio = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;

[ViewVariables]
public readonly Dictionary<ProtoId<BwoinkChannelPrototype>, Dictionary<NetUserId, PlayerChannelProperties>>
PlayerChannels = new();

/// <summary>
/// Contains the ratelimits for each userchannel we send a typing status to.
/// </summary>
private Dictionary<NetUserId, TimeSpan> _typingRateLimits = new();

/// <summary>
/// Dictionary that contains the sounds to play for a specified channel, source may be null.
/// </summary>
public readonly Dictionary<ProtoId<BwoinkChannelPrototype>, IAudioSource?> CachedSounds = new();

/// <summary>
/// Called whenever our prototypes change, or a full state update is applied.
/// </summary>
public event Action? ReloadedData;

/// <summary>
/// Called whenever we receieve a <see cref="MsgBwoinkTypings"/>
/// </summary>
public event Action? TypingsUpdated;

public override void Initialize()
{
base.Initialize();
_netManager.RegisterNetMessage<MsgBwoinkNonAdmin>(BwoinkAttempted);
_netManager.RegisterNetMessage<MsgBwoink>(AdminBwoinkAttempted);
_netManager.RegisterNetMessage<MsgBwoinkSyncRequest>();
_netManager.RegisterNetMessage<MsgBwoinkSync>(SyncBwoinks);
_netManager.RegisterNetMessage<MsgBwoinkTypingUpdate>();
_netManager.RegisterNetMessage<MsgBwoinkTypings>(SyncTypings);

_adminManager.AdminStatusUpdated += StatusUpdated;
}

private void SyncTypings(MsgBwoinkTypings message)
{
TypingStatuses[message.Channel] = message.Typings;
TypingsUpdated?.Invoke();
}

private void StatusUpdated()
{
RequestSync();
}

public PlayerChannelProperties GetOrCreatePlayerPropertiesForChannel(ProtoId<BwoinkChannelPrototype> channel, NetUserId userId)
{
PlayerChannels.TryAdd(channel, new Dictionary<NetUserId, PlayerChannelProperties>());

if (PlayerChannels[channel].TryGetValue(userId, out var value))
return value;

PlayerChannels[channel].Add(userId, new PlayerChannelProperties());
return PlayerChannels[channel][userId];
}

protected override void UpdatedChannels()
{
foreach (var (key, channel) in ProtoCache)
{
foreach (var feature in channel.Features)
{
if (feature is not SoundOnMessage soundOnMessage)
continue;

if (CachedSounds.TryGetValue(key, out var cachedSound))
cachedSound?.Dispose();

var sound = _audio.CreateAudioSource(_res.GetResource<AudioResource>(soundOnMessage.Sound.Path));
if (sound != null)
sound.Global = true;

CachedSounds[key] = sound;
break;
}
}

ReloadedData?.Invoke();
}

private void SyncBwoinks(MsgBwoinkSync message)
{
Log.Info($"Received full state! {message.Conversations.Count} channels with {message.Conversations.Values.Select(x => x.Count).Count()} conversations.");
Conversations = message.Conversations;
ReloadedData?.Invoke();
}

private void AdminBwoinkAttempted(MsgBwoink message)
{
InvokeMessageReceived(message.Channel,
message.Target,
message.Message.Content,
message.Message.SenderId,
message.Message.Sender,
message.Message.Flags);
}

private void BwoinkAttempted(MsgBwoinkNonAdmin message)
{
// This one is targeted to us, so we use our local session as the target.
// ReSharper disable once NullableWarningSuppressionIsUsed
// "The user Id of the local player. This will be null on the server.".
// Null suppression because we will only ever receive this while being connected. If it is null, something has gone wrong.
InvokeMessageReceived(message.Channel,
_playerManager.LocalUser!.Value,
message.Message.Content,
message.Message.SenderId,
message.Message.Sender,
message.Message.Flags);
}

public void SendMessageNonAdmin(ProtoId<BwoinkChannelPrototype> channel, string text)
{
_netManager.ClientSendMessage(new MsgBwoinkNonAdmin()
{
// We can leave all of this null since the server will set all of this anyways.
Message = new BwoinkMessage(string.Empty, null, DateTime.UtcNow, text, MessageFlags.None),
Channel = channel,
});
}

public void SendMessageAdmin(BwoinkChannelPrototype channel, NetUserId user, string text)
{
_netManager.ClientSendMessage(new MsgBwoink()
{
// We can leave all of this null since the server will set all of this anyways.
Message = new BwoinkMessage(string.Empty, null, DateTime.UtcNow, text, MessageFlags.None),
Channel = channel,
Target = user,
});
}

/// <summary>
/// Updates your typing status for the client, this results in a <see cref="MsgBwoinkTypingUpdate"/> message.
/// </summary>
/// <remarks>
/// This method has an internal ratelimit of 8 seconds. This ratelimit only applies for setting the typing state to true.
/// </remarks>
public void SetTypingStatus(ProtoId<BwoinkChannelPrototype> channel, bool typing, NetUserId? userChannel)
{
const int rateLimit = 3;

userChannel ??= _playerManager.LocalUser!.Value;
// Check ratelimit
if (_typingRateLimits.TryGetValue(userChannel.Value, out var limit))
{
if (typing && _gameTiming.RealTime < limit)
return;

if (!typing)
{
// We are sending a "stopped typing" message. So: Remove current rate limit key so the next "i am typing" message can get through.
_typingRateLimits.Remove(userChannel.Value);
}
else
{
_typingRateLimits[userChannel.Value] = _gameTiming.RealTime + TimeSpan.FromSeconds(rateLimit);
}
}
else
{
_typingRateLimits.Add(userChannel.Value, _gameTiming.RealTime + TimeSpan.FromSeconds(rateLimit));
}

_netManager.ClientSendMessage(new MsgBwoinkTypingUpdate()
{
IsTyping = typing,
Channel = channel,
ChannelUserId = userChannel.Value,
});
}

/// <summary>
/// Requests a full re-sync of all conversations we have. There is no locking so calling this while conversations are on-going may result in dropped or duplicated messages.
/// </summary>
public void RequestSync()
{
// TODO: Maybe locking???
Log.Info("Resetting Bwoink state!");
_netManager.ClientSendMessage(new MsgBwoinkSyncRequest());
}
}

public sealed class PlayerChannelProperties
{
public DateTime LastMessage { get; set; } = DateTime.MinValue;
public int Unread { get; set; } = 0;
}
42 changes: 0 additions & 42 deletions Content.Client/Administration/Systems/BwoinkSystem.cs

This file was deleted.

12 changes: 1 addition & 11 deletions Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the quick actions for player interactions. Like follow, kick, ban, etc. I'm afraid that this will ruin the experience that admins have come to expect with the ahelp menu, also the player panel is frankly extremely slow and is unusable till data from the database is loaded in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image its already on the todo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, wasn't aware it was on the todo list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion on the actions list is there's definitely a way better way to do it than hardcoded buttons lmao

Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,8 @@
</BoxContainer>
</SplitContainer>
<BoxContainer Orientation="Horizontal" SetHeight="30" >
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Access="Public" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
<Button Visible="False" Name="Playerpanel" Text="{Loc 'admin-player-actions-player-panel'}" />
</BoxContainer>
</SplitContainer>
</PanelContainer>
Expand Down
Loading