522 lines
15 KiB
C#
522 lines
15 KiB
C#
using bgs.protocol;
|
|
using DiIiS_NA.Core.Helpers.Hash;
|
|
using DiIiS_NA.Core.Logging;
|
|
using DiIiS_NA.GameServer.ClientSystem;
|
|
using DiIiS_NA.LoginServer.AccountsSystem;
|
|
using DiIiS_NA.LoginServer.Base;
|
|
using DiIiS_NA.LoginServer.ChannelSystem;
|
|
using DiIiS_NA.LoginServer.Objects;
|
|
using DiIiS_NA.LoginServer.ServicesSystem;
|
|
using DotNetty.Transport.Channels;
|
|
using DotNetty.Transport.Channels.Sockets;
|
|
using Google.ProtocolBuffers;
|
|
using Google.ProtocolBuffers.DescriptorProtos;
|
|
using Google.ProtocolBuffers.Descriptors;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Security;
|
|
using System.Threading.Tasks;
|
|
using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Text;
|
|
|
|
namespace DiIiS_NA.LoginServer.Battle
|
|
{
|
|
public class BattleClient : SimpleChannelInboundHandler<BNetPacket>, IRpcChannel
|
|
{
|
|
private static readonly Logger Logger = LogManager.CreateLogger(nameof(BattleClient));
|
|
|
|
public Dictionary<uint, uint> Services { get; private set; }
|
|
public ISocketChannel SocketConnection { get; private set; }
|
|
public IChannelHandlerContext Connect { get; private set; }
|
|
public bool AuthenticationStatus = false;
|
|
public ClientLocale ClientLanguage = ClientLocale.EN_US;
|
|
public IRpcController ListenerController;
|
|
private uint _tokenCounter = 0;
|
|
public static NO_RESPONSE NoResponse = NO_RESPONSE.CreateBuilder().Build();
|
|
private readonly Dictionary<int, RPCCallBack> _pendingResponses = new(); // TODO: Check usage and remove if not needed
|
|
public bgs.protocol.v2.Attribute AttributeOfServer { get; set; }
|
|
|
|
public Account Account { get; set; }
|
|
public const byte SERVICE_REPLY = 0xFE;
|
|
public SslStream Ssl = null;
|
|
private static int _requestServiceId = 0;
|
|
private static int _responseServiceId = 254;
|
|
//public object clientLock = new object();
|
|
public readonly object ServiceLock = new();
|
|
public object MessageLock = new();
|
|
private ulong _listenerId; // last targeted rpc object.
|
|
private ConcurrentDictionary<ulong, ulong> MappedObjects { get; set; }
|
|
public bool GuildChannelsRevealed = false;
|
|
public string GameTeamTag = "";
|
|
public readonly ulong Cid = 0;
|
|
|
|
#region current channel
|
|
|
|
public readonly Dictionary<ulong, Channel> Channels = new();
|
|
|
|
public readonly List<Channel> ChatChannels = new();
|
|
public Channel PartyChannel; //Used for all non game related messages
|
|
public Channel GameChannel; //Used for all game related messages
|
|
|
|
public GameClient InGameClient { get; set; }
|
|
|
|
private Channel _currentChannel;
|
|
public Channel CurrentChannel
|
|
{
|
|
get => _currentChannel ??= Channels.Values.FirstOrDefault(c => !c.IsChatChannel);
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
if (_currentChannel != null)
|
|
Channels.Remove(_currentChannel.DynamicId);
|
|
//Logger.Trace("Client removed from CurrentChannel: {0}, setting new CurrentChannel to {1}", this._currentChannel, this.Channels.FirstOrDefault().Value);
|
|
_currentChannel = Channels.FirstOrDefault().Value;
|
|
}
|
|
else if (!Channels.ContainsKey(value.DynamicId))
|
|
{
|
|
Channels.Add(value.DynamicId, value);
|
|
_currentChannel = value;
|
|
}
|
|
else
|
|
_currentChannel = value;
|
|
}
|
|
}
|
|
|
|
|
|
public void SendServerWhisper(string text)
|
|
{
|
|
if (text.Trim() == string.Empty) return;
|
|
|
|
var notification = bgs.protocol.notification.v1.Notification.CreateBuilder()
|
|
.SetTargetId(Account.GameAccount.BnetEntityId)
|
|
.SetType("WHISPER")
|
|
.SetSenderId(Account.GameAccount.BnetEntityId)
|
|
.SetSenderAccountId(Account.BnetEntityId)
|
|
.AddAttribute(bgs.protocol.Attribute.CreateBuilder().SetName("whisper")
|
|
.SetValue(Variant.CreateBuilder().SetStringValue(text).Build()).Build()).Build();
|
|
|
|
MakeRpc((lid) => bgs.protocol.notification.v1.NotificationListener.CreateStub(this).
|
|
OnNotificationReceived(new HandlerController()
|
|
{
|
|
ListenerId = lid
|
|
}, notification, callback => { }));
|
|
}
|
|
|
|
public void SendServerMessage(string text)
|
|
{
|
|
InGameClient.SendMessage(new BroadcastTextMessage()
|
|
{
|
|
Field0 = text
|
|
});
|
|
}
|
|
|
|
public void LeaveAllChannels()
|
|
{
|
|
List<Channel> channels = Channels.Values.ToList();
|
|
foreach (var channel in channels)
|
|
{
|
|
try
|
|
{
|
|
channel.RemoveMember(this, Channel.RemoveReason.Left);
|
|
}
|
|
catch { }
|
|
}
|
|
Channels.Clear();
|
|
}
|
|
|
|
#endregion
|
|
|
|
public BattleClient(ISocketChannel socketChannel, DotNetty.Handlers.Tls.TlsHandler tls)
|
|
{
|
|
SocketConnection = socketChannel;
|
|
Services = new Dictionary<uint, uint>();
|
|
MappedObjects = new ConcurrentDictionary<ulong, ulong>();
|
|
if (SocketConnection.Active)
|
|
Logger.Trace("Client - $[green]$ {0} $[/]$ - successfully encrypted the connection", socketChannel.RemoteAddress);
|
|
}
|
|
|
|
protected override void ChannelRead0(IChannelHandlerContext ctx, BNetPacket msg)
|
|
{
|
|
Connect = ctx;
|
|
Header header = msg.GetHeader();
|
|
byte[] payload = (byte[])msg.GetPayload();
|
|
|
|
if (msg.GetHeader().ServiceId == _responseServiceId)
|
|
{
|
|
if (_pendingResponses.Count == 0) return;
|
|
RPCCallBack done = _pendingResponses[(int)header.Token];
|
|
if (done != null)
|
|
{
|
|
var service = Service.GetByID(header.ServiceId);
|
|
if (service != null)
|
|
{
|
|
IMessage message = DescriptorProto.ParseFrom(payload);
|
|
done.Action(message);
|
|
_pendingResponses.Remove((int)header.Token);
|
|
}
|
|
else
|
|
Logger.Debug(
|
|
$"Incoming Response: Unable to identify service (id: {header.ServiceId}, hash: 0x{header.ServiceHash:04X})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var service = Service.GetByID(Service.GetByHash(header.ServiceHash));
|
|
if (header.ServiceHash != 2119327385)
|
|
if (service != null)
|
|
{
|
|
#region All service hashes
|
|
|
|
/*
|
|
AccountService - 1658456209
|
|
AccountNotify - 1423956503
|
|
AuthenticationClient - 1898188341
|
|
AuthenticationServer - 233634817
|
|
ChallengeNotify - 3151632159
|
|
ChannelService - 3073563442
|
|
ChannelSubscriber - 3213656212
|
|
ChannelVoiceService - 2559626750
|
|
ClubMembershipListener - 724851067
|
|
ClubMembershipService - 2495170438 - Нужен
|
|
ConnectionService - 1698982289
|
|
DiagService - 3111080599
|
|
FriendsService - 2749215165
|
|
FriendsNotify - 1864735251
|
|
PresenceListener - 2299181151
|
|
PresenceService - 4194801407
|
|
ReportService - 2091868617
|
|
Resources - 3971904954
|
|
SessionListener - 2145610546
|
|
SessionService - 510168069
|
|
SocialNetworkService - 1910276758
|
|
SocialNetworkListener - 3506428651
|
|
UserManagerService - 1041835658
|
|
UserManagerNotify - 3162975266
|
|
WhisperListener - 1072006302
|
|
WhisperService - 3240634617
|
|
ChannelMembershipService - 2119327385
|
|
ChannelMembershipListener - 25167806
|
|
ChannelService_v2 - 2039298513
|
|
ChannelListener_v2 - 451225222
|
|
ReportService_v2 - 977410299
|
|
VoiceService_v2 - 4117798472
|
|
|
|
AccountService - 0x62DA0891
|
|
AccountNotify - 0x54DFDA17
|
|
AuthenticationClient - 0x71240E35
|
|
AuthenticationServer - 0x0DECFC01
|
|
ChallengeNotify - 0xBBDA171F
|
|
ChannelService - 0xB732DB32
|
|
ChannelSubscriber - 0xBF8C8094
|
|
ChannelVoiceService - 0x9890CDFE
|
|
ClubMembershipListener - 0x2B34597B
|
|
ClubMembershipService - 0x94B94786
|
|
ConnectionService - 0x65446991
|
|
DiagService - 0xB96F5297
|
|
FriendsService - 0xA3DDB1BD
|
|
FriendsNotify - 0x6F259A13
|
|
PresenceListener - 0x890AB85F
|
|
PresenceService - 0xFA0796FF
|
|
ReportService - 0x7CAF61C9
|
|
Resources - ECBE75BA
|
|
SessionListener - 0x7FE36B32
|
|
SessionService - 0x1E688C05
|
|
SocialNetworkService - 0x71DC8296
|
|
SocialNetworkListener - 0xD0FFDAEB
|
|
UserManagerService - 0x3E19268A
|
|
UserManagerNotify - 0xBC872C22
|
|
WhisperListener - 0x3FE5849E
|
|
WhisperService - 0xC12828F9
|
|
ChannelMembershipService - 0x7E525E99
|
|
ChannelMembershipListener - 0x018007BE
|
|
ChannelService_v2 - 0x798D39D1
|
|
ChannelListener_v2 - 0x1AE52686
|
|
ReportService_v2 - 0x3A4218FB
|
|
VoiceService_v2 - 0xF5709E48
|
|
*/
|
|
|
|
#endregion
|
|
|
|
MethodDescriptor method =
|
|
service.DescriptorForType.Methods.Single(m => GetMethodId(m) == header.MethodId);
|
|
IMessage proto = service.GetRequestPrototype(method);
|
|
IBuilder builder = proto.WeakCreateBuilderForType();
|
|
IMessage message = builder.WeakMergeFrom(ByteString.CopyFrom(payload)).WeakBuild();
|
|
try
|
|
{
|
|
lock (service)
|
|
{
|
|
HandlerController controller = new()
|
|
{
|
|
Client = this,
|
|
LastCallHeader = header,
|
|
Status = 0,
|
|
ListenerId = 0
|
|
};
|
|
#if DEBUG
|
|
Logger.Debug(
|
|
$"Call: $[underline white]${service.GetType().Name}$[/]$, Service hash: $[underline white]${header.ServiceHash}$[/]$, Method: $[underline white]${method.Name}$[/]$, ID: $[olive]${header.MethodId}$[/]$");
|
|
|
|
#endif
|
|
|
|
service.CallMethod(method, controller, message,
|
|
(IMessage m) => { SendResponse(ctx, (int)header.Token, m, controller.Status); });
|
|
}
|
|
}
|
|
catch (NotImplementedException)
|
|
{
|
|
Logger.Warn("Unimplemented service method:$[red]$ {0}.{1} $[/]$", service.GetType().Name, method.Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.Warn(
|
|
$"Client is calling unconnected service (id: {header.ServiceId}, hash: {header.ServiceHash} Method id: {header.MethodId})");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Platform enum for clients.
|
|
/// </summary>
|
|
public enum ClientPlatform
|
|
{
|
|
Unknown,
|
|
Invalid,
|
|
Win,
|
|
Mac
|
|
}
|
|
|
|
/// <summary>
|
|
/// Locale enum for clients.
|
|
/// </summary>
|
|
public enum ClientLocale
|
|
{
|
|
/// <summary>
|
|
/// Unknown client locale state.
|
|
/// </summary>
|
|
Unknown,
|
|
/// <summary>
|
|
/// Invalid client locale.
|
|
/// </summary>
|
|
Invalid,
|
|
/// <summary>
|
|
/// Deutsch.
|
|
/// </summary>
|
|
DE_DE,
|
|
/// <summary>
|
|
/// English (EU)
|
|
/// </summary>
|
|
EN_GB,
|
|
/// <summary>
|
|
/// English (Singapore)
|
|
/// </summary>
|
|
EN_SG,
|
|
/// <summary>
|
|
/// English (US)
|
|
/// </summary>
|
|
EN_US,
|
|
/// <summary>
|
|
/// Espanol
|
|
/// </summary>
|
|
ES_ES,
|
|
/// <summary>
|
|
/// Espanol (Mexico)
|
|
/// </summary>
|
|
ES_MX,
|
|
/// <summary>
|
|
/// French
|
|
/// </summary>
|
|
FR_FR,
|
|
/// <summary>
|
|
/// Italian
|
|
/// </summary>
|
|
IT_IT,
|
|
/// <summary>
|
|
/// Korean
|
|
/// </summary>
|
|
KO_KR,
|
|
/// <summary>
|
|
/// Polish
|
|
/// </summary>
|
|
PL_PL,
|
|
/// <summary>
|
|
/// Portuguese
|
|
/// </summary>
|
|
PT_PT,
|
|
/// <summary>
|
|
/// Portuguese (Brazil)
|
|
/// </summary>
|
|
PT_BR,
|
|
/// <summary>
|
|
/// Russian
|
|
/// </summary>
|
|
RU_RU,
|
|
/// <summary>
|
|
/// Turkish
|
|
/// </summary>
|
|
TR_TR,
|
|
/// <summary>
|
|
/// Chinese
|
|
/// </summary>
|
|
ZH_CN,
|
|
/// <summary>
|
|
/// Chinese (Taiwan)
|
|
/// </summary>
|
|
ZH_TW
|
|
}
|
|
public virtual void MakeTargetedRpc(RPCObject targetObject, Action<ulong> rpc)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
//lock (this.clientLock)
|
|
//{
|
|
try
|
|
{
|
|
if (SocketConnection == null || !SocketConnection.Active) return;
|
|
var listenerId = GetRemoteObjectId(targetObject.DynamicId);
|
|
Logger.Debug("[$[underline yellow]$RPC: {0}$[/]$] Method: $[underline white]${1}$[/]$ Target: $[underline white]${2}$[/]$ " +
|
|
"[localId: $[underline white]${3}$[/]$, remoteId: $[underline white]${4}$[/]$].", GetType().Name, rpc.Method.Name,
|
|
targetObject.ToString(), targetObject.DynamicId, listenerId);
|
|
|
|
rpc(listenerId);
|
|
}
|
|
catch { }
|
|
//}
|
|
});
|
|
}
|
|
public virtual void MakeRpc(Action<ulong> rpc)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
//lock (this.clientLock)
|
|
//{
|
|
try
|
|
{
|
|
if (SocketConnection == null || !SocketConnection.Active) return;
|
|
Logger.Debug("[$[underline yellow]$RPC: {0}$[/]$] Method: $[underline yellow]${1}$[/]$ Target: $[underline red]$N/A$[/]$", GetType().Name, rpc.Method.Name);
|
|
rpc(0);
|
|
}
|
|
catch { }
|
|
//}
|
|
});
|
|
}
|
|
public void CallMethod(MethodDescriptor method, IRpcController controller, IMessage request, IMessage responsePrototype, Action<IMessage> done)
|
|
{
|
|
var serviceName = method.Service.FullName;
|
|
string str = "";
|
|
|
|
if (serviceName.ToLower().Contains("gamerequestlistener"))
|
|
str = "bnet.protocol.matchmaking.GameRequestListener";
|
|
else
|
|
str = method.Service.Options.UnknownFields[90000].LengthDelimitedList[0].ToStringUtf8().Remove(0, 2);
|
|
var serviceHash = StringHashHelper.HashIdentity(str);
|
|
|
|
if (!Services.ContainsKey(serviceHash))
|
|
{
|
|
Logger.Warn("Service not found for client {0} [$[underline blue]$0x{1}$[/]$].", serviceName, serviceHash.ToString("X8"));
|
|
// in english: "Service not found for client {0} [0x{1}]."
|
|
return;
|
|
}
|
|
|
|
uint status = 0;
|
|
|
|
if (controller is HandlerController handlerController)
|
|
{
|
|
status = handlerController.Status;
|
|
_listenerId = handlerController.ListenerId;
|
|
}
|
|
|
|
var serviceId = Services[serviceHash];
|
|
var token = _tokenCounter++;
|
|
SendRequest(Connect, serviceHash, GetMethodId(method), token, request, (uint)_listenerId, status);
|
|
}
|
|
public static void SendRequest(IChannelHandlerContext ctx, uint serviceHash, uint methodId, uint token, IMessage request, uint listenerId, uint status)
|
|
{
|
|
Header.Builder builder = Header.CreateBuilder();
|
|
builder.SetServiceId((uint)_requestServiceId);
|
|
builder.SetServiceHash(serviceHash);
|
|
builder.SetMethodId(methodId);
|
|
if (listenerId != 0)
|
|
builder.SetObjectId(listenerId);
|
|
builder.SetToken(token);
|
|
builder.SetSize((uint)request.SerializedSize);
|
|
builder.SetStatus(status);
|
|
|
|
ctx.Channel.WriteAndFlushAsync(new BNetPacket(builder.Build(), request));
|
|
}
|
|
/// <param name="localObjectId">The local objectId.</param>
|
|
/// <param name="remoteObjectId">The remote objectId over client.</param>
|
|
public void MapLocalObjectId(ulong localObjectId, ulong remoteObjectId)
|
|
{
|
|
try
|
|
{
|
|
MappedObjects[localObjectId] = remoteObjectId;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.WarnException(e, "MapLocalObjectID()");
|
|
}
|
|
}
|
|
|
|
/// <param name="localObjectId"></param>
|
|
public void UnmapLocalObjectId(ulong localObjectId)
|
|
{
|
|
try
|
|
{
|
|
MappedObjects.TryRemove(localObjectId, out _);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.WarnException(e, "UnmapLocalObjectID()");
|
|
}
|
|
}
|
|
public ulong GetRemoteObjectId(ulong localObjectId)
|
|
{
|
|
return localObjectId != 0 ? MappedObjects[localObjectId] : 0;
|
|
}
|
|
public static uint GetMethodId(MethodDescriptor method)
|
|
{
|
|
try
|
|
{
|
|
return (uint)method.Options.UnknownFields[90000].LengthDelimitedList[0].ToByteArray()[1];
|
|
}
|
|
catch
|
|
{
|
|
return (uint)(method.Index) + 1;
|
|
}
|
|
}
|
|
public static void SendResponse(IChannelHandlerContext ctx, int token, IMessage response, uint status)
|
|
{
|
|
Header.Builder builder = Header.CreateBuilder();
|
|
builder.SetServiceId((uint)_responseServiceId);
|
|
builder.SetToken((uint)token);
|
|
builder.SetStatus(status);
|
|
if (response != null)
|
|
builder.SetSize((uint)response.SerializedSize);
|
|
|
|
ctx.Channel.WriteAndFlushAsync(new BNetPacket(builder.Build(), response));
|
|
}
|
|
public void SendMotd()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(LoginServerConfig.Instance.Motd) || !LoginServerConfig.Instance.MotdEnabled)
|
|
return;
|
|
Logger.Debug($"Motd sent to {Account.BattleTag}.");
|
|
SendServerWhisper(LoginServerConfig.Instance.Motd);
|
|
}
|
|
|
|
public override void ChannelInactive(IChannelHandlerContext context)
|
|
{
|
|
DisconnectClient();
|
|
base.ChannelInactive(context);
|
|
}
|
|
|
|
private void DisconnectClient()
|
|
{
|
|
if (Account != null && Account.GameAccount != null) Account.GameAccount.LoggedInClient = null;
|
|
PlayerManager.PlayerDisconnected(this);
|
|
}
|
|
}
|
|
} |