2021-09-10 14:36:49 +00:00
using Lumina.Excel.GeneratedSheets ;
2020-10-23 21:24:32 +00:00
using MessagePack ;
using Sodium ;
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2021-06-07 00:45:39 +00:00
using System.Diagnostics ;
2020-10-23 21:24:32 +00:00
using System.Linq ;
using System.Net ;
using System.Net.Sockets ;
using System.Text ;
using System.Threading ;
using System.Threading.Channels ;
using System.Threading.Tasks ;
2021-09-10 14:36:49 +00:00
using Dalamud.Game ;
2021-04-05 19:09:04 +00:00
using Dalamud.Game.Text ;
using Dalamud.Game.Text.SeStringHandling ;
using Dalamud.Game.Text.SeStringHandling.Payloads ;
2021-09-10 14:36:49 +00:00
using Dalamud.Logging ;
2020-10-23 21:24:32 +00:00
using XIVChatCommon ;
2020-11-18 04:29:34 +00:00
using XIVChatCommon.Message ;
using XIVChatCommon.Message.Client ;
using XIVChatCommon.Message.Server ;
2020-10-23 21:24:32 +00:00
namespace XIVChatPlugin {
2021-07-05 01:09:38 +00:00
internal class Server : IDisposable {
2021-06-07 00:45:39 +00:00
private const int MaxMessageLength = 500 ;
private static readonly string [ ] PublicPrefixes = {
"/t " ,
"/tell " ,
"/reply " ,
"/r " ,
"/say " ,
"/s " ,
"/shout " ,
"/sh " ,
"/yell " ,
"/y " ,
} ;
2021-01-20 04:02:26 +00:00
private readonly Plugin _plugin ;
2020-10-23 21:24:32 +00:00
2021-06-07 00:45:39 +00:00
private readonly Stopwatch _sendWatch = new ( ) ;
2021-02-13 00:43:57 +00:00
private readonly CancellationTokenSource _tokenSource = new ( ) ;
private readonly ConcurrentQueue < string > _toGame = new ( ) ;
2020-10-23 21:24:32 +00:00
2021-02-13 00:43:57 +00:00
private readonly ConcurrentDictionary < Guid , BaseClient > _clients = new ( ) ;
2021-07-05 01:09:38 +00:00
internal IReadOnlyDictionary < Guid , BaseClient > Clients = > this . _clients ;
internal readonly Channel < Tuple < BaseClient , Channel < bool > > > PendingClients = Channel . CreateUnbounded < Tuple < BaseClient , Channel < bool > > > ( ) ;
2020-10-23 21:24:32 +00:00
2021-02-13 00:43:57 +00:00
private readonly HashSet < Guid > _waitingForFriendList = new ( ) ;
2020-10-23 21:24:32 +00:00
2021-02-13 00:43:57 +00:00
private readonly LinkedList < ServerMessage > _backlog = new ( ) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
private TcpListener ? _listener ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
private bool _sendPlayerData ;
2021-02-13 00:43:57 +00:00
private readonly ConcurrentQueue < Guid > _awaitingPlayerData = new ( ) ;
private readonly ConcurrentQueue < Guid > _awaitingAvailability = new ( ) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
private volatile bool _running ;
private bool Running = > this . _running ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
private InputChannel _currentChannel = InputChannel . Say ;
2020-10-23 21:24:32 +00:00
2020-11-01 01:25:52 +00:00
private const int MaxMessageSize = 128_000 ;
2020-10-23 21:24:32 +00:00
2021-07-05 01:09:38 +00:00
internal Server ( Plugin plugin ) {
2021-06-07 00:45:39 +00:00
this . _plugin = plugin ;
2021-01-20 04:02:26 +00:00
if ( this . _plugin . Config . KeyPair = = null ) {
2020-10-23 21:24:32 +00:00
this . RegenerateKeyPair ( ) ;
}
2021-06-07 00:45:39 +00:00
this . _sendWatch . Start ( ) ;
2021-01-20 04:02:26 +00:00
this . _plugin . Functions . ReceiveFriendList + = this . OnReceiveFriendList ;
2020-10-23 21:24:32 +00:00
}
2020-10-25 15:41:38 +00:00
private void SpawnPairingModeTask ( ) {
Task . Run ( async ( ) = > {
// delay for 10 seconds because of the jank way we cancel below to prevent port bind issues
await Task . Delay ( 10_000 ) ;
const int multicastPort = 17444 ;
2020-11-06 20:19:42 +00:00
using var udp = new UdpClient ( ) ;
udp . Client . SetSocketOption ( SocketOptionLevel . Socket , SocketOptionName . ReuseAddress , true ) ;
udp . Client . Bind ( new IPEndPoint ( IPAddress . Any , multicastPort ) ) ;
2020-10-25 15:41:38 +00:00
var multicastAddr = IPAddress . Parse ( "224.0.0.147" ) ;
udp . JoinMulticastGroup ( multicastAddr ) ;
2021-09-10 14:36:49 +00:00
SeString ? lastPlayerName = null ;
2020-10-25 15:41:38 +00:00
2020-11-01 01:25:52 +00:00
Task < UdpReceiveResult > ? receiveTask = null ;
2020-10-25 15:41:38 +00:00
while ( this . Running ) {
2021-01-20 04:02:26 +00:00
if ( ! this . _plugin . Config . PairingMode ) {
2020-10-25 15:41:38 +00:00
await Task . Delay ( 5_000 ) ;
continue ;
}
2021-09-10 14:36:49 +00:00
var playerName = this . _plugin . ClientState . LocalPlayer ? . Name ;
2020-10-25 15:41:38 +00:00
if ( playerName ! = null ) {
lastPlayerName = playerName ;
}
if ( lastPlayerName = = null ) {
await Task . Delay ( 5_000 ) ;
continue ;
}
2020-11-01 01:25:52 +00:00
receiveTask ? ? = udp . ReceiveAsync ( ) ;
2020-10-25 15:41:38 +00:00
var result = await Task . WhenAny (
receiveTask ,
Task . Delay ( 1_500 )
) ;
if ( result ! = receiveTask ) {
if ( ! this . Running ) {
udp . Close ( ) ;
}
continue ;
}
var recv = await receiveTask ;
receiveTask = null ;
var data = recv . Buffer ;
if ( data . Length ! = 1 | | data [ 0 ] ! = 14 ) {
continue ;
}
2021-09-10 14:36:49 +00:00
var utf8 = Encoding . UTF8 . GetBytes ( lastPlayerName . TextValue ) ;
2021-01-20 04:02:26 +00:00
var portBytes = BitConverter . GetBytes ( this . _plugin . Config . Port ) . Reverse ( ) . ToArray ( ) ;
var key = this . _plugin . Config . KeyPair ! . PublicKey ;
2020-10-25 15:41:38 +00:00
// magic + string length + string + port + key
var payload = new byte [ 1 + 1 + utf8 . Length + portBytes . Length + key . Length ] ; // assuming names can only be 32 bytes here
payload [ 0 ] = 14 ;
2021-01-07 21:26:27 +00:00
payload [ 1 ] = ( byte ) utf8 . Length ;
2020-10-25 15:41:38 +00:00
Array . Copy ( utf8 , 0 , payload , 2 , utf8 . Length ) ;
Array . Copy ( portBytes , 0 , payload , 2 + utf8 . Length , portBytes . Length ) ;
Array . Copy ( key , 0 , payload , 2 + utf8 . Length + portBytes . Length , key . Length ) ;
await udp . SendAsync ( payload , payload . Length , recv . RemoteEndPoint ) ;
}
PluginLog . Log ( "Scan response thread done" ) ;
} ) ;
}
2020-10-23 21:24:32 +00:00
private async void OnReceiveFriendList ( List < Player > friends ) {
2020-11-01 15:31:56 +00:00
var msg = new ServerPlayerList ( PlayerListType . Friend , friends . ToArray ( ) ) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
foreach ( var id in this . _waitingForFriendList ) {
2020-10-23 21:24:32 +00:00
if ( ! this . Clients . TryGetValue ( id , out var client ) ) {
continue ;
}
2021-02-13 00:43:57 +00:00
await client . Queue . Writer . WriteAsync ( msg ) ;
2020-10-23 21:24:32 +00:00
}
2021-01-20 04:02:26 +00:00
this . _waitingForFriendList . Clear ( ) ;
2020-10-23 21:24:32 +00:00
}
2021-07-05 01:09:38 +00:00
internal void Spawn ( ) {
2021-01-20 04:02:26 +00:00
var port = this . _plugin . Config . Port ;
2020-10-23 21:24:32 +00:00
Task . Run ( async ( ) = > {
2021-01-20 04:02:26 +00:00
this . _listener = new TcpListener ( IPAddress . Any , port ) ;
this . _listener . Start ( ) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
this . _running = true ;
2020-10-23 21:24:32 +00:00
PluginLog . Log ( "Running..." ) ;
2020-10-25 15:41:38 +00:00
this . SpawnPairingModeTask ( ) ;
2021-01-20 04:02:26 +00:00
while ( ! this . _tokenSource . IsCancellationRequested ) {
var conn = await this . _listener . GetTcpClient ( this . _tokenSource ) ;
if ( conn = = null ) {
continue ;
}
var client = new TcpConnected ( conn ) ;
this . SpawnClientTask ( client , true ) ;
2020-10-23 21:24:32 +00:00
}
2020-11-01 01:25:52 +00:00
2021-01-20 04:02:26 +00:00
this . _running = false ;
2020-10-23 21:24:32 +00:00
} ) ;
}
2021-07-05 01:09:38 +00:00
internal void RegenerateKeyPair ( ) {
2021-01-20 04:02:26 +00:00
this . _plugin . Config . KeyPair = PublicKeyBox . GenerateKeyPair ( ) ;
this . _plugin . Config . Save ( ) ;
2020-10-23 21:24:32 +00:00
}
2021-07-05 01:09:38 +00:00
internal void OnChat ( XivChatType type , uint senderId , ref SeString sender , ref SeString message , ref bool isHandled ) {
2020-10-23 21:24:32 +00:00
if ( isHandled ) {
return ;
}
2021-01-07 21:26:27 +00:00
var chatCode = new ChatCode ( ( ushort ) type ) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
if ( ! this . _plugin . Config . SendBattle & & chatCode . IsBattle ( ) ) {
2020-10-23 21:24:32 +00:00
return ;
}
var chunks = new List < Chunk > ( ) ;
2021-01-20 04:02:26 +00:00
var colour = this . _plugin . Functions . GetChannelColour ( chatCode ) ? ? chatCode . DefaultColour ( ) ;
2020-10-26 03:21:16 +00:00
2020-10-23 21:24:32 +00:00
if ( sender . Payloads . Count > 0 ) {
2020-11-23 18:16:58 +00:00
var format = this . FormatFor ( chatCode . Type ) ;
2021-06-07 00:45:39 +00:00
if ( format is { IsPresent : true } ) {
2020-11-01 15:31:56 +00:00
chunks . Add ( new TextChunk ( format . Before ) {
2020-10-26 03:21:16 +00:00
FallbackColour = colour ,
2020-10-23 21:24:32 +00:00
} ) ;
2020-10-26 03:21:16 +00:00
chunks . AddRange ( ToChunks ( sender , colour ) ) ;
2020-11-01 15:31:56 +00:00
chunks . Add ( new TextChunk ( format . After ) {
2020-10-26 03:21:16 +00:00
FallbackColour = colour ,
2020-10-23 21:24:32 +00:00
} ) ;
}
}
2020-10-26 03:21:16 +00:00
chunks . AddRange ( ToChunks ( message , colour ) ) ;
2020-10-23 21:24:32 +00:00
2020-11-01 15:31:56 +00:00
var msg = new ServerMessage (
DateTime . UtcNow ,
2021-01-07 21:26:27 +00:00
( ChatType ) type ,
2020-11-01 15:31:56 +00:00
sender . Encode ( ) ,
message . Encode ( ) ,
chunks
) ;
2020-10-23 21:24:32 +00:00
2021-01-20 04:02:26 +00:00
this . _backlog . AddLast ( msg ) ;
while ( this . _backlog . Count > this . _plugin . Config . BacklogCount ) {
this . _backlog . RemoveFirst ( ) ;
2020-10-23 21:24:32 +00:00
}
2021-01-20 04:02:26 +00:00
foreach ( var client in this . _clients . Values ) {
2020-10-23 21:24:32 +00:00
client . Queue . Writer . TryWrite ( msg ) ;
}
}
2021-09-10 14:36:49 +00:00
internal void OnFrameworkUpdate ( Framework framework1 ) {
var player = this . _plugin . ClientState . LocalPlayer ;
2021-02-17 00:31:32 +00:00
if ( player ! = null & & this . _sendPlayerData ) {
this . BroadcastPlayerData ( ) ;
this . _sendPlayerData = false ;
}
2021-02-13 00:43:57 +00:00
2021-02-17 00:31:32 +00:00
while ( this . _awaitingPlayerData . TryDequeue ( out var id ) ) {
if ( ! this . Clients . TryGetValue ( id , out var client ) ) {
continue ;
2021-02-13 00:43:57 +00:00
}
2021-02-17 00:31:32 +00:00
2021-02-17 00:50:36 +00:00
var playerData = ( Encodable ? ) this . GeneratePlayerData ( ) ? ? EmptyPlayerData . Instance ;
2021-02-17 00:31:32 +00:00
client . Queue . Writer . TryWrite ( playerData ) ;
2021-02-13 00:43:57 +00:00
}
while ( this . _awaitingAvailability . TryDequeue ( out var id ) ) {
if ( ! this . Clients . TryGetValue ( id , out var client ) | | client . Handshake = = null ) {
continue ;
}
var available = player ! = null ;
client . Queue . Writer . TryWrite ( new Availability ( available ) ) ;
2020-10-23 21:24:32 +00:00
}
2021-06-07 00:45:39 +00:00
int time ;
if ( this . _toGame . TryPeek ( out var peek ) & & PublicPrefixes . Any ( prefix = > peek . StartsWith ( prefix ) ) ) {
time = 1_000 ;
} else if ( this . _currentChannel is InputChannel . Tell or InputChannel . Say or InputChannel . Shout or InputChannel . Yell ) {
time = 1_000 ;
} else {
time = 250 ;
}
if ( this . _sendWatch . Elapsed < TimeSpan . FromMilliseconds ( time ) ) {
return ;
}
2021-01-20 04:02:26 +00:00
if ( ! this . _toGame . TryDequeue ( out var message ) ) {
2020-10-23 21:24:32 +00:00
return ;
}
2021-06-07 00:45:39 +00:00
this . _sendWatch . Restart ( ) ;
2021-01-20 04:02:26 +00:00
this . _plugin . Functions . ProcessChatBox ( message ) ;
2020-10-23 21:24:32 +00:00
}
2020-11-01 01:25:52 +00:00
private static readonly IReadOnlyList < byte > Magic = new byte [ ] {
14 , 20 , 67 ,
2020-10-26 20:00:06 +00:00
} ;
2021-01-20 04:02:26 +00:00
internal void SpawnClientTask ( BaseClient client , bool requiresMagic ) {
var id = Guid . NewGuid ( ) ;
this . _clients [ id ] = client ;
2020-10-23 21:24:32 +00:00
Task . Run ( async ( ) = > {
2021-01-20 04:02:26 +00:00
if ( requiresMagic ) {
// get ready for reading magic bytes
var magic = new byte [ Magic . Count ] ;
var read = 0 ;
// only listen for magic for five seconds
using var cts = new CancellationTokenSource ( ) ;
cts . CancelAfter ( TimeSpan . FromSeconds ( 5 ) ) ;
// read magic bytes
while ( read < magic . Length ) {
if ( cts . IsCancellationRequested ) {
return ;
}
2020-10-26 20:00:06 +00:00
2021-01-20 04:02:26 +00:00
read + = await client . ReadAsync ( magic , read , magic . Length - read , cts . Token ) ;
}
2020-10-26 20:00:06 +00:00
2021-01-20 04:02:26 +00:00
// ignore this connection if incorrect magic bytes
if ( ! magic . SequenceEqual ( Magic ) ) {
2020-10-26 20:00:06 +00:00
return ;
}
}
2021-01-20 04:02:26 +00:00
var handshake = await KeyExchange . ServerHandshake ( this . _plugin . Config . KeyPair ! , client ) ;
client . Handshake = handshake ;
2020-10-23 21:24:32 +00:00
// if this public key isn't trusted, prompt first
2021-01-20 04:02:26 +00:00
if ( ! this . _plugin . Config . TrustedKeys . Values . Any ( entry = > entry . Item2 . SequenceEqual ( handshake . RemotePublicKey ) ) ) {
2020-10-26 20:00:06 +00:00
// if configured to not accept new clients, reject connection
2021-01-20 04:02:26 +00:00
if ( ! this . _plugin . Config . AcceptNewClients ) {
2020-10-26 20:00:06 +00:00
return ;
}
2020-10-23 21:24:32 +00:00
var accepted = Channel . CreateBounded < bool > ( 1 ) ;
2021-01-20 04:02:26 +00:00
await this . PendingClients . Writer . WriteAsync ( Tuple . Create ( client , accepted ) , this . _tokenSource . Token ) ;
if ( ! await accepted . Reader . ReadAsync ( this . _tokenSource . Token ) ) {
2020-10-23 21:24:32 +00:00
return ;
}
}
2021-01-20 04:02:26 +00:00
client . Connected = true ;
2020-10-23 21:24:32 +00:00
2021-02-13 00:43:57 +00:00
// queue sending availability for this client
this . _awaitingAvailability . Enqueue ( id ) ;
2020-10-23 21:24:32 +00:00
2021-02-13 00:43:57 +00:00
// queue sending player data for this client
this . _awaitingPlayerData . Enqueue ( id ) ;
2020-10-23 21:24:32 +00:00
// send current channel
try {
2021-01-20 04:02:26 +00:00
var channel = this . _currentChannel ;
2020-11-01 01:25:52 +00:00
await SecretMessage . SendSecretMessage (
2021-01-20 04:02:26 +00:00
client ,
2020-11-01 01:25:52 +00:00
handshake . Keys . tx ,
new ServerChannel (
channel ,
this . LocalisedChannelName ( channel )
) ,
2021-01-20 04:02:26 +00:00
this . _tokenSource . Token
2020-11-01 01:25:52 +00:00
) ;
2020-10-23 21:24:32 +00:00
} catch ( Exception ex ) {
PluginLog . LogError ( $"Could not send message: {ex.Message}" ) ;
}
var listen = Task . Run ( async ( ) = > {
2021-01-20 04:02:26 +00:00
while ( this . _clients . TryGetValue ( id , out var client ) & & client . Connected & & ! client . TokenSource . IsCancellationRequested ) {
2020-10-23 21:24:32 +00:00
byte [ ] msg ;
try {
2021-01-20 04:02:26 +00:00
msg = await SecretMessage . ReadSecretMessage ( client , handshake . Keys . rx , client . TokenSource . Token ) ;
2020-10-23 21:24:32 +00:00
} catch ( SocketException ex ) when ( ex . SocketErrorCode = = SocketError . TimedOut ) {
continue ;
} catch ( Exception ex ) {
PluginLog . LogError ( $"Could not read message: {ex.Message}" ) ;
continue ;
}
2021-06-07 01:02:59 +00:00
await this . ProcessMessage ( id , client , msg ) ;
2020-10-23 21:24:32 +00:00
}
} ) ;
2021-09-10 14:36:49 +00:00
this . _plugin . Events . FireNewClientEvent ( id , client ) ;
2021-01-20 04:02:26 +00:00
while ( this . _clients . TryGetValue ( id , out var client ) & & client . Connected & & ! client . TokenSource . IsCancellationRequested ) {
2020-10-23 21:24:32 +00:00
try {
var msg = await client . Queue . Reader . ReadAsync ( client . TokenSource . Token ) ;
2021-01-20 04:02:26 +00:00
await SecretMessage . SendSecretMessage ( client , handshake . Keys . tx , msg , client . TokenSource . Token ) ;
2020-10-23 21:24:32 +00:00
} catch ( Exception ex ) {
PluginLog . LogError ( $"Could not send message: {ex.Message}" ) ;
}
}
2021-01-20 04:02:26 +00:00
client . Disconnect ( ) ;
2020-10-23 21:24:32 +00:00
await listen ;
2021-01-20 04:02:26 +00:00
this . _clients . TryRemove ( id , out _ ) ;
2020-10-23 21:24:32 +00:00
PluginLog . Log ( $"Client thread ended: {id}" ) ;
2020-10-26 20:00:06 +00:00
} ) . ContinueWith ( _ = > {
2021-01-24 23:01:48 +00:00
this . RemoveClient ( id ) ;
2020-11-01 01:25:52 +00:00
} ) ;
2020-10-23 21:24:32 +00:00
}
2021-01-24 23:01:48 +00:00
internal void RemoveClient ( Guid id ) {
if ( ! this . _clients . TryRemove ( id , out var client ) ) {
return ;
}
client . Disconnect ( ) ;
}
2021-06-07 01:02:59 +00:00
private async Task ProcessMessage ( Guid id , BaseClient client , byte [ ] msg ) {
2021-01-20 04:02:26 +00:00
var op = ( ClientOperation ) msg [ 0 ] ;
var payload = new byte [ msg . Length - 1 ] ;
Array . Copy ( msg , 1 , payload , 0 , payload . Length ) ;
switch ( op ) {
case ClientOperation . Ping :
try {
2021-02-13 00:43:57 +00:00
await client . Queue . Writer . WriteAsync ( Pong . Instance ) ;
2021-01-20 04:02:26 +00:00
} catch ( Exception ex ) {
PluginLog . LogError ( $"Could not send message: {ex.Message}" ) ;
}
break ;
case ClientOperation . Message :
var clientMessage = ClientMessage . Decode ( payload ) ;
2021-06-07 00:50:18 +00:00
var sanitised = clientMessage . Content
. Replace ( "\r\n" , " " )
. Replace ( '\r' , ' ' )
. Replace ( '\n' , ' ' ) ;
foreach ( var part in Wrap ( sanitised ) ) {
2021-01-20 04:02:26 +00:00
this . _toGame . Enqueue ( part ) ;
}
break ;
case ClientOperation . Shutdown :
client . Disconnect ( ) ;
break ;
case ClientOperation . Backlog :
// ReSharper disable once LocalVariableHidesMember
var backlog = ClientBacklog . Decode ( payload ) ;
var backlogMessages = new List < ServerMessage > ( ) ;
var node = this . _backlog . Last ;
while ( node ! = null ) {
if ( backlogMessages . Count > = backlog . Amount ) {
break ;
}
backlogMessages . Add ( node . Value ) ;
node = node . Previous ;
}
if ( ! client . GetPreference ( ClientPreference . BacklogNewestMessagesFirst , false ) ) {
backlogMessages . Reverse ( ) ;
}
await SendBacklogs ( backlogMessages . ToArray ( ) , client ) ;
break ;
case ClientOperation . CatchUp :
var catchUp = ClientCatchUp . Decode ( payload ) ;
// I'm not sure why this needs to be done, but apparently it does
var after = catchUp . After . AddMilliseconds ( 1 ) ;
var msgs = this . MessagesAfter ( after ) ;
if ( client . GetPreference ( ClientPreference . BacklogNewestMessagesFirst , false ) ) {
msgs = msgs . Reverse ( ) ;
}
await SendBacklogs ( msgs , client ) ;
break ;
case ClientOperation . PlayerList :
var playerList = ClientPlayerList . Decode ( payload ) ;
if ( playerList . Type = = PlayerListType . Friend ) {
this . _waitingForFriendList . Add ( id ) ;
if ( ! this . _plugin . Functions . RequestingFriendList & & ! this . _plugin . Functions . RequestFriendList ( ) ) {
2021-09-10 14:36:49 +00:00
this . _plugin . ChatGui . PrintError ( $"[{this._plugin.Name}] Please open your friend list to enable friend list support. You should only need to do this on initial install or after updates." ) ;
2021-01-20 04:02:26 +00:00
}
}
break ;
case ClientOperation . Preferences :
var preferences = ClientPreferences . Decode ( payload ) ;
client . Preferences = preferences ;
break ;
case ClientOperation . Channel :
var channel = ClientChannel . Decode ( payload ) ;
this . _plugin . Functions . ChangeChatChannel ( channel . Channel ) ;
break ;
}
}
2021-07-05 01:09:38 +00:00
internal class NameFormatting {
internal string Before { get ; private set ; } = string . Empty ;
internal string After { get ; private set ; } = string . Empty ;
internal bool IsPresent { get ; private set ; } = true ;
2020-12-13 02:49:44 +00:00
2021-07-05 01:09:38 +00:00
internal static NameFormatting Empty ( ) {
2021-02-13 00:43:57 +00:00
return new ( ) {
2020-12-13 02:49:44 +00:00
IsPresent = false ,
} ;
}
2021-07-05 01:09:38 +00:00
internal static NameFormatting Of ( string before , string after ) {
2021-02-13 00:43:57 +00:00
return new ( ) {
2020-12-13 02:49:44 +00:00
Before = before ,
After = after ,
} ;
}
}
2020-11-23 18:16:58 +00:00
2021-02-13 00:43:57 +00:00
private Dictionary < ChatType , NameFormatting > Formats { get ; } = new ( ) ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
private NameFormatting ? FormatFor ( ChatType type ) {
2020-10-23 21:24:32 +00:00
if ( this . Formats . TryGetValue ( type , out var cached ) ) {
return cached ;
}
2021-09-10 14:36:49 +00:00
var logKind = this . _plugin . DataManager . GetExcelSheet < LogKind > ( ) ! . GetRow ( ( ushort ) type ) ;
2020-10-23 21:24:32 +00:00
if ( logKind = = null ) {
return null ;
}
2021-09-10 14:36:49 +00:00
var format = ( SeString ) logKind . Format ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
static bool IsStringParam ( Payload payload , byte num ) {
2020-12-22 14:05:10 +00:00
var data = payload . Encode ( ) ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
return data . Length > = 5 & & data [ 1 ] = = 0x29 & & data [ 4 ] = = num + 1 ;
}
2020-10-23 21:24:32 +00:00
2021-09-10 14:36:49 +00:00
var firstStringParam = format . Payloads . FindIndex ( payload = > IsStringParam ( payload , 1 ) ) ;
var secondStringParam = format . Payloads . FindIndex ( payload = > IsStringParam ( payload , 2 ) ) ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
if ( firstStringParam = = - 1 | | secondStringParam = = - 1 ) {
return NameFormatting . Empty ( ) ;
}
2020-10-23 21:24:32 +00:00
2021-09-10 14:36:49 +00:00
var before = format . Payloads
2020-11-23 18:16:58 +00:00
. GetRange ( 0 , firstStringParam )
. Where ( payload = > payload is ITextProvider )
. Cast < ITextProvider > ( )
. Select ( text = > text . Text ) ;
2021-09-10 14:36:49 +00:00
var after = format . Payloads
2020-11-23 18:16:58 +00:00
. GetRange ( firstStringParam + 1 , secondStringParam - firstStringParam )
. Where ( payload = > payload is ITextProvider )
. Cast < ITextProvider > ( )
. Select ( text = > text . Text ) ;
var nameFormatting = NameFormatting . Of (
string . Join ( "" , before ) ,
string . Join ( "" , after )
) ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
this . Formats [ type ] = nameFormatting ;
2020-10-23 21:24:32 +00:00
2020-11-23 18:16:58 +00:00
return nameFormatting ;
2020-10-23 21:24:32 +00:00
}
2021-01-20 04:02:26 +00:00
private static async Task SendBacklogs ( IEnumerable < ServerMessage > messages , BaseClient client ) {
2021-02-11 05:19:28 +00:00
const int defaultSize = 5 + SecretMessage . NonceSize + SecretMessage . MacSize ;
var size = defaultSize ;
2020-10-23 21:24:32 +00:00
var responseMessages = new List < ServerMessage > ( ) ;
async Task SendBacklog ( ) {
var resp = new ServerBacklog ( responseMessages . ToArray ( ) ) ;
try {
2021-02-13 00:43:57 +00:00
await client . Queue . Writer . WriteAsync ( resp ) ;
2020-10-23 21:24:32 +00:00
} catch ( Exception ex ) {
PluginLog . LogError ( $"Could not send backlog: {ex.Message}" ) ;
}
}
foreach ( var catchUpMessage in messages ) {
// FIXME: this is very gross
var len = MessagePackSerializer . Serialize ( catchUpMessage ) . Length ;
// send message if it would've gone over length
2020-11-01 01:25:52 +00:00
if ( size + len > = MaxMessageSize ) {
2020-10-23 21:24:32 +00:00
await SendBacklog ( ) ;
2021-02-11 05:19:28 +00:00
size = defaultSize ;
2020-10-23 21:24:32 +00:00
responseMessages . Clear ( ) ;
}
2020-11-01 01:25:52 +00:00
2020-10-23 21:24:32 +00:00
size + = len ;
responseMessages . Add ( catchUpMessage ) ;
}
if ( responseMessages . Count > 0 ) {
await SendBacklog ( ) ;
}
}
2020-11-01 01:25:52 +00:00
private static IEnumerable < Chunk > ToChunks ( SeString msg , uint? defaultColour ) {
2020-10-23 21:24:32 +00:00
var chunks = new List < Chunk > ( ) ;
var italic = false ;
var foreground = new Stack < uint > ( ) ;
var glow = new Stack < uint > ( ) ;
void Append ( string text ) {
2020-11-01 15:31:56 +00:00
chunks . Add ( new TextChunk ( text ) {
2020-10-23 21:24:32 +00:00
FallbackColour = defaultColour ,
2021-06-07 01:02:59 +00:00
Foreground = foreground . Count > 0 ? foreground . Peek ( ) : null ,
2021-06-07 00:45:39 +00:00
Glow = glow . Count > 0 ? glow . Peek ( ) : null ,
2020-10-23 21:24:32 +00:00
Italic = italic ,
} ) ;
}
foreach ( var payload in msg . Payloads ) {
switch ( payload . Type ) {
case PayloadType . EmphasisItalic :
2021-01-07 21:26:27 +00:00
var newStatus = ( ( EmphasisItalicPayload ) payload ) . IsEnabled ;
2020-10-23 21:24:32 +00:00
italic = newStatus ;
break ;
case PayloadType . UIForeground :
2021-01-07 21:26:27 +00:00
var foregroundPayload = ( UIForegroundPayload ) payload ;
2020-10-23 21:24:32 +00:00
if ( foregroundPayload . IsEnabled ) {
foreground . Push ( foregroundPayload . UIColor . UIForeground ) ;
} else if ( foreground . Count > 0 ) {
foreground . Pop ( ) ;
}
2020-11-01 01:25:52 +00:00
2020-10-23 21:24:32 +00:00
break ;
case PayloadType . UIGlow :
2021-01-07 21:26:27 +00:00
var glowPayload = ( UIGlowPayload ) payload ;
2020-10-23 21:24:32 +00:00
if ( glowPayload . IsEnabled ) {
glow . Push ( glowPayload . UIColor . UIGlow ) ;
} else if ( glow . Count > 0 ) {
glow . Pop ( ) ;
}
2020-11-01 01:25:52 +00:00
2020-10-23 21:24:32 +00:00
break ;
case PayloadType . AutoTranslateText :
2020-11-01 01:25:52 +00:00
chunks . Add ( new IconChunk {
2020-11-01 15:31:56 +00:00
index = 54 ,
2020-11-01 01:25:52 +00:00
} ) ;
2021-01-07 21:26:27 +00:00
var autoText = ( ( AutoTranslatePayload ) payload ) . Text ;
2020-10-23 21:24:32 +00:00
Append ( autoText . Substring ( 2 , autoText . Length - 4 ) ) ;
2020-11-01 01:25:52 +00:00
chunks . Add ( new IconChunk {
2020-11-01 15:31:56 +00:00
index = 55 ,
2020-11-01 01:25:52 +00:00
} ) ;
2020-10-23 21:24:32 +00:00
break ;
case PayloadType . Icon :
2021-01-07 21:26:27 +00:00
var index = ( ( IconPayload ) payload ) . Icon ;
2020-11-01 01:25:52 +00:00
chunks . Add ( new IconChunk {
2021-01-07 21:26:27 +00:00
index = ( byte ) index ,
2020-11-01 01:25:52 +00:00
} ) ;
2020-10-23 21:24:32 +00:00
break ;
case PayloadType . Unknown :
2021-01-07 21:26:27 +00:00
var rawPayload = ( RawPayload ) payload ;
2020-10-23 21:24:32 +00:00
if ( rawPayload . Data [ 1 ] = = 0x13 ) {
foreground . Pop ( ) ;
glow . Pop ( ) ;
}
2020-11-01 01:25:52 +00:00
2020-10-23 21:24:32 +00:00
break ;
2020-11-01 15:31:56 +00:00
default :
if ( payload is ITextProvider textProvider ) {
Append ( textProvider . Text ) ;
}
break ;
2020-10-23 21:24:32 +00:00
}
}
return chunks ;
}
2021-01-20 04:02:26 +00:00
private IEnumerable < ServerMessage > MessagesAfter ( DateTime time ) = > this . _backlog . Where ( msg = > msg . Timestamp > time ) . ToArray ( ) ;
2020-10-23 21:24:32 +00:00
2020-11-01 01:25:52 +00:00
private static IEnumerable < string > Wrap ( string input ) {
2021-06-07 00:45:39 +00:00
if ( input . Length < = MaxMessageLength ) {
2020-11-01 01:25:52 +00:00
return new [ ] {
input ,
} ;
2020-10-23 21:24:32 +00:00
}
string prefix = string . Empty ;
if ( input . StartsWith ( "/" ) ) {
var space = input . IndexOf ( ' ' ) ;
if ( space ! = - 1 ) {
2021-09-10 14:36:49 +00:00
prefix = input [ . . space ] ;
2021-06-07 00:45:39 +00:00
// handle wrapping tells
if ( prefix is "/tell" or "/t" ) {
var tellSpace = input . IndexOfCount ( ' ' , 3 ) ;
if ( tellSpace ! = - 1 ) {
2021-09-10 14:36:49 +00:00
prefix = input [ . . tellSpace ] ;
input = input [ ( tellSpace + 1 ) . . ] ;
2020-10-23 21:24:32 +00:00
}
2021-06-07 00:45:39 +00:00
} else {
2021-09-10 14:36:49 +00:00
input = input [ ( space + 1 ) . . ] ;
2020-10-23 21:24:32 +00:00
}
}
}
2021-06-07 00:45:39 +00:00
return NativeTools . Wrap ( input , MaxMessageLength )
. Select ( text = > $"{prefix} {text}" )
. ToArray ( ) ;
2020-10-23 21:24:32 +00:00
}
2021-02-17 00:50:36 +00:00
private void BroadcastMessage ( Encodable message ) {
2020-10-23 21:24:32 +00:00
foreach ( var client in this . Clients . Values ) {
2021-02-13 00:43:57 +00:00
client . Queue . Writer . TryWrite ( message ) ;
2020-10-23 21:24:32 +00:00
}
}
private string LocalisedChannelName ( InputChannel channel ) {
2020-11-01 01:25:52 +00:00
uint rowId = channel switch {
InputChannel . Tell = > 3 ,
InputChannel . Say = > 1 ,
InputChannel . Party = > 4 ,
InputChannel . Alliance = > 17 ,
InputChannel . Yell = > 16 ,
InputChannel . Shout = > 2 ,
InputChannel . FreeCompany = > 7 ,
InputChannel . PvpTeam = > 19 ,
InputChannel . NoviceNetwork = > 18 ,
InputChannel . CrossLinkshell1 = > 20 ,
InputChannel . CrossLinkshell2 = > 300 ,
InputChannel . CrossLinkshell3 = > 301 ,
InputChannel . CrossLinkshell4 = > 302 ,
InputChannel . CrossLinkshell5 = > 303 ,
InputChannel . CrossLinkshell6 = > 304 ,
InputChannel . CrossLinkshell7 = > 305 ,
InputChannel . CrossLinkshell8 = > 306 ,
InputChannel . Linkshell1 = > 8 ,
InputChannel . Linkshell2 = > 9 ,
InputChannel . Linkshell3 = > 10 ,
InputChannel . Linkshell4 = > 11 ,
InputChannel . Linkshell5 = > 12 ,
InputChannel . Linkshell6 = > 13 ,
InputChannel . Linkshell7 = > 14 ,
InputChannel . Linkshell8 = > 15 ,
_ = > 0 ,
2020-10-23 21:24:32 +00:00
} ;
2020-11-01 01:25:52 +00:00
2021-09-10 14:36:49 +00:00
return this . _plugin . DataManager . GetExcelSheet < LogFilter > ( ) ! . GetRow ( rowId ) ? . Name ? ? string . Empty ;
2020-10-23 21:24:32 +00:00
}
2021-07-05 01:09:38 +00:00
internal void OnChatChannelChange ( uint channel ) {
2021-01-07 21:26:27 +00:00
var inputChannel = ( InputChannel ) channel ;
2021-01-20 04:02:26 +00:00
this . _currentChannel = inputChannel ;
2020-10-23 21:24:32 +00:00
var localisedName = this . LocalisedChannelName ( inputChannel ) ;
var msg = new ServerChannel ( inputChannel , localisedName ) ;
this . BroadcastMessage ( msg ) ;
}
private void BroadcastAvailability ( bool available ) {
this . BroadcastMessage ( new Availability ( available ) ) ;
}
2020-11-01 01:25:52 +00:00
private PlayerData ? GeneratePlayerData ( ) {
2021-09-10 14:36:49 +00:00
var player = this . _plugin . ClientState . LocalPlayer ;
2020-10-23 21:24:32 +00:00
if ( player = = null ) {
return null ;
}
var homeWorld = player . HomeWorld . GameData . Name ;
var currentWorld = player . CurrentWorld . GameData . Name ;
2021-09-10 14:36:49 +00:00
var territory = this . _plugin . DataManager . GetExcelSheet < TerritoryType > ( ) ! . GetRow ( this . _plugin . ClientState . TerritoryType ) ;
2020-11-01 15:31:56 +00:00
var location = territory ? . PlaceName ? . Value ? . Name ? ? "???" ;
2021-09-10 14:36:49 +00:00
var name = player . Name . TextValue ;
2020-10-23 21:24:32 +00:00
return new PlayerData ( homeWorld , currentWorld , location , name ) ;
}
private void BroadcastPlayerData ( ) {
2021-02-17 00:50:36 +00:00
var playerData = ( Encodable ? ) this . GeneratePlayerData ( ) ? ? EmptyPlayerData . Instance ;
2020-10-23 21:24:32 +00:00
this . BroadcastMessage ( playerData ) ;
}
2021-09-10 14:36:49 +00:00
internal void OnLogIn ( object? sender , EventArgs e ) {
2020-10-23 21:24:32 +00:00
this . BroadcastAvailability ( true ) ;
// send player data on next framework update
2021-01-20 04:02:26 +00:00
this . _sendPlayerData = true ;
2020-10-23 21:24:32 +00:00
}
2021-09-10 14:36:49 +00:00
internal void OnLogOut ( object? sender , EventArgs e ) {
2020-10-23 21:24:32 +00:00
this . BroadcastAvailability ( false ) ;
this . BroadcastPlayerData ( ) ;
}
2021-09-10 14:36:49 +00:00
internal void OnTerritoryChange ( object? sender , ushort territoryId ) = > this . _sendPlayerData = true ;
2020-10-23 21:24:32 +00:00
public void Dispose ( ) {
// stop accepting new clients
2021-01-20 04:02:26 +00:00
this . _tokenSource . Cancel ( ) ;
foreach ( var client in this . _clients . Values ) {
2020-10-23 21:24:32 +00:00
Task . Run ( async ( ) = > {
// tell clients we're shutting down
if ( client . Handshake ! = null ) {
try {
2021-01-20 04:02:26 +00:00
await SecretMessage . SendSecretMessage ( client , client . Handshake . Keys . tx , ServerShutdown . Instance ) ;
2021-01-07 21:26:27 +00:00
} catch ( Exception ) {
2021-01-20 04:02:26 +00:00
// ignored
2021-01-07 21:26:27 +00:00
}
2020-10-23 21:24:32 +00:00
}
2020-11-01 01:25:52 +00:00
2020-10-23 21:24:32 +00:00
// cancel threads for open clients
client . TokenSource . Cancel ( ) ;
} ) ;
}
2020-11-01 01:25:52 +00:00
2021-01-20 04:02:26 +00:00
this . _plugin . Functions . ReceiveFriendList - = this . OnReceiveFriendList ;
2020-11-06 01:38:38 +00:00
}
2020-10-23 21:24:32 +00:00
}
internal static class TcpListenerExt {
2021-07-05 01:09:38 +00:00
internal static async Task < TcpClient ? > GetTcpClient ( this TcpListener listener , CancellationTokenSource source ) {
2021-09-10 14:36:49 +00:00
await using ( source . Token . Register ( listener . Stop ) ) {
2020-10-23 21:24:32 +00:00
try {
var client = await listener . AcceptTcpClientAsync ( ) . ConfigureAwait ( false ) ;
return client ;
2020-11-01 01:25:52 +00:00
} catch ( ObjectDisposedException ) {
2020-10-23 21:24:32 +00:00
// Token was canceled - swallow the exception and return null
if ( source . Token . IsCancellationRequested ) {
return null ;
}
2020-11-01 01:25:52 +00:00
throw ;
2020-10-23 21:24:32 +00:00
}
}
}
}
}