diff --git a/pack.ps1 b/pack.ps1 index 014377c..9c8b1c4 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -10,7 +10,7 @@ if ($Help) exit 0 } -$Projects = "Core", "TreasureSolver"; +$Projects = @("Core"); echo "> Packing projects: $Projects" echo "> Output path: $Output" @@ -46,11 +46,6 @@ foreach ($Project in $Projects) continue; } - if ( $OtherProjectsDll.Contains($Filename)) - { - continue; - } - echo "Copying $File to $Dir..." copy $File $Dir } diff --git a/src/Core/DBIPlayer.cs b/src/Core/DBIPlayer.cs index c439e74..44cfb64 100644 --- a/src/Core/DBIPlayer.cs +++ b/src/Core/DBIPlayer.cs @@ -1,6 +1,4 @@ using System; -using Com.Ankama.Dofus.Server.Connection.Protocol; -using Com.Ankama.Dofus.Server.Game.Protocol.Common; using DofusBatteriesIncluded.Plugins.Core.Player; using Microsoft.Extensions.Logging; @@ -42,3 +40,23 @@ internal void SetCurrentCharacter(Character character) internal void OnPlayerChangeCompleted() => CurrentCharacterChangeCompleted?.Invoke(this, CurrentCharacter); } + +public class IdentificationResponse +{ + public ResultOneofCase ResultCase { get; set; } + public Types.Success Success { get; set; } + + public enum ResultOneofCase + { + Success + } + + public class Types + { + public class Success + { + public long AccountId { get; set; } + public string AccountNickname { get; set; } + } + } +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedMessage.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedMessage.cs new file mode 100644 index 0000000..967846b --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedMessage.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; + +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +public class ObfuscatedMessage +{ + readonly object _message; + readonly PropertyInfo _contentOneofCaseProperty; + readonly PropertyInfo _contentProperty; + readonly PropertyInfo _contentPointerProperty; + + public ObfuscatedMessage(object message, PropertyInfo contentOneofCaseProperty, PropertyInfo contentProperty, PropertyInfo contentPointerProperty) + { + _message = message; + _contentOneofCaseProperty = contentOneofCaseProperty; + _contentProperty = contentProperty; + _contentPointerProperty = contentPointerProperty; + } + + public ObfuscatedMessageContentOneOfCase ContentOneOfCase { + get { + object result = _contentOneofCaseProperty.GetValue(_message); + string resultString = result?.ToString(); + return resultString == null ? ObfuscatedMessageContentOneOfCase.None : Enum.Parse(resultString); + } + } + public IntPtr? ContentPointer { + get { + object content = _contentProperty.GetValue(_message); + object contentPointer = _contentPointerProperty?.GetValue(content); + return (IntPtr?)contentPointer; + } + } +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedMessageContentOneOfCase.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedMessageContentOneOfCase.cs new file mode 100644 index 0000000..4a9573f --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedMessageContentOneOfCase.cs @@ -0,0 +1,9 @@ +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +public enum ObfuscatedMessageContentOneOfCase +{ + None, + Event, + Response, + Request +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedMessageReader.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedMessageReader.cs new file mode 100644 index 0000000..f8fec30 --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedMessageReader.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using System.Reflection; +using DofusBatteriesIncluded.Plugins.Core.Extensions; +using Google.Protobuf; +using Il2CppSystem.Threading; +using Object = Il2CppSystem.Object; + +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +class ObfuscatedMessageReader +{ + const string GameProtocolAssemblyName = "Ankama.Dofus.Protocol.Game"; + const string GameProtocolAssemblyPath = $"BepInEx/interop/{GameProtocolAssemblyName}.dll"; + + readonly Type _obfuscatedMessageType; + readonly PropertyInfo _contentOneofCaseProperty; + readonly PropertyInfo _contentProperty; + readonly PropertyInfo _contentPointerProperty; + + public ObfuscatedMessageReader() + { + Thread.Sleep(5000); + + Assembly gameAssembly = AppDomain.CurrentDomain.LoadAssemblyIfNotLoaded(GameProtocolAssemblyName, GameProtocolAssemblyPath); + + Type contentOneOfCaseNestedInMessageType = FindContentOneOfCaseTypeNestedInMessageType(gameAssembly); + _obfuscatedMessageType = contentOneOfCaseNestedInMessageType.DeclaringType!; + _contentOneofCaseProperty = _obfuscatedMessageType.GetProperties().First(p => p.PropertyType == contentOneOfCaseNestedInMessageType); + _contentProperty = _obfuscatedMessageType.GetProperties().First(p => p.PropertyType == typeof(Object)); + _contentPointerProperty = _contentProperty.PropertyType.GetProperty("Pointer"); + } + + public ObfuscatedMessage GetMessage(IMessage message) + { + object messageInstance = Activator.CreateInstance(_obfuscatedMessageType, message.Pointer); + return new ObfuscatedMessage(messageInstance, _contentOneofCaseProperty, _contentProperty, _contentPointerProperty); + } + + static Type FindContentOneOfCaseTypeNestedInMessageType(Assembly assembly) => + assembly.GetTypes() + .Where(t => t.IsEnum && t.DeclaringType != null) + .Single( + t => + { + // Find the enum containing values None, Event, Request, Response: it is the ObfuscatedMessageContentOneofCase enum defined by the Message class + string[] values = t.GetEnumValues().Cast().Select(e => e.ToString()).ToArray(); + return values.Length == 4 && values.Contains("None") && values.Contains("Event") && values.Contains("Response") && values.Contains("Request"); + } + ); +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedResponse.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedResponse.cs new file mode 100644 index 0000000..287dad7 --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedResponse.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +public class ObfuscatedResponse +{ + readonly object _response; + readonly PropertyInfo _contentOneofCaseProperty; + readonly IReadOnlyCollection _contentProperties; + + public ObfuscatedResponse(object response, PropertyInfo contentOneofCaseProperty, IReadOnlyCollection contentProperties) + { + _response = response; + _contentOneofCaseProperty = contentOneofCaseProperty; + _contentProperties = contentProperties; + } + + public ObfuscatedResponseContentOneOfCase GetContentOneOfCaseInResponse { + get { + object result = _contentOneofCaseProperty.GetValue(_response); + string resultString = result?.ToString(); + return resultString == null ? ObfuscatedResponseContentOneOfCase.None : Enum.Parse(resultString); + } + } + + public object Identification => GetNonNullContentProperty(); + public object Pong => GetNonNullContentProperty(); + public object SelectServer => GetNonNullContentProperty(); + public object ForceAccount => GetNonNullContentProperty(); + public object FriendList => GetNonNullContentProperty(); + public object AcquaintanceServersResponse => GetNonNullContentProperty(); + + object GetNonNullContentProperty() => _contentProperties.Select(p => p.GetValue(_response)).FirstOrDefault(r => r != null); +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedResponseContentOneOfCase.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedResponseContentOneOfCase.cs new file mode 100644 index 0000000..f6ca9a0 --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedResponseContentOneOfCase.cs @@ -0,0 +1,12 @@ +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +public enum ObfuscatedResponseContentOneOfCase +{ + None, + Identification, + Pong, + SelectServer, + ForceAccount, + FriendList, + AcquaintanceServersResponse +} diff --git a/src/Core/Deobfuscation/Protocol/ObfuscatedResponseReader.cs b/src/Core/Deobfuscation/Protocol/ObfuscatedResponseReader.cs new file mode 100644 index 0000000..a362864 --- /dev/null +++ b/src/Core/Deobfuscation/Protocol/ObfuscatedResponseReader.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DofusBatteriesIncluded.Plugins.Core.Extensions; + +namespace DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; + +class ObfuscatedResponseReader +{ + const string ConnectionProtocolAssemblyName = "Ankama.Dofus.Protocol.Connection"; + const string ConnectionProtocolAssemblyPath = $"BepInEx/interop/{ConnectionProtocolAssemblyName}.dll"; + + readonly Type _obfuscatedResponseType; + readonly PropertyInfo _contentOneofCaseProperty; + readonly IReadOnlyCollection _contentProperties; + + public ObfuscatedResponseReader() + { + Assembly connectionAssembly = AppDomain.CurrentDomain.LoadAssemblyIfNotLoaded(ConnectionProtocolAssemblyName, ConnectionProtocolAssemblyPath); + + Type contentOneOfCaseNestedInResponseType = FindContentOneOfCaseTypeNestedInResponseType(connectionAssembly); + _obfuscatedResponseType = contentOneOfCaseNestedInResponseType.DeclaringType!; + _contentOneofCaseProperty = _obfuscatedResponseType.GetProperties().First(p => p.PropertyType == contentOneOfCaseNestedInResponseType); + _contentProperties = _obfuscatedResponseType.GetProperties() + .Where(p => p.PropertyType.Assembly == connectionAssembly && p.PropertyType != contentOneOfCaseNestedInResponseType) + .ToArray(); + } + + public ObfuscatedResponse GetResponse(IntPtr pointer) + { + object responseInstance = Activator.CreateInstance(_obfuscatedResponseType, pointer); + return new ObfuscatedResponse(responseInstance, _contentOneofCaseProperty, _contentProperties); + } + + static Type FindContentOneOfCaseTypeNestedInResponseType(Assembly assembly) => + assembly.GetTypes() + .Where(t => t.IsEnum && t.DeclaringType != null) + .Single( + t => + { + // Find the enum containing values None, Event, Request, Response: it is the ObfuscatedMessageContentOneofCase enum defined by the Message class + string[] values = t.GetEnumValues().Cast().Select(e => e.ToString()).ToArray(); + return values.Length == 7 + && values.Contains("None") + && values.Contains("Identification") + && values.Contains("Pong") + && values.Contains("SelectServer") + && values.Contains("ForceAccount") + && values.Contains("FriendList") + && values.Contains("AcquaintanceServersResponse"); + } + ); +} diff --git a/src/Core/Extensions/AppDomainExtensions.cs b/src/Core/Extensions/AppDomainExtensions.cs new file mode 100644 index 0000000..a3bd64c --- /dev/null +++ b/src/Core/Extensions/AppDomainExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace DofusBatteriesIncluded.Plugins.Core.Extensions; + +static class AppDomainExtensions +{ + public static Assembly LoadAssemblyIfNotLoaded(this AppDomain appDomain, string assemblyName, string assemblyPath) + { + Assembly assembly = appDomain.GetAssemblies().SingleOrDefault(a => a.GetName().Name == assemblyName); + + if (assembly == null) + { + // assembly not loaded + assembly = Assembly.LoadFrom(assemblyPath); + } + + return assembly; + } +} diff --git a/src/Core/Player/Character.cs b/src/Core/Player/Character.cs new file mode 100644 index 0000000..d374d9d --- /dev/null +++ b/src/Core/Player/Character.cs @@ -0,0 +1,8 @@ +namespace DofusBatteriesIncluded.Plugins.Core.Player; + +public class Character +{ + public long Id { get; set; } + public string Name { get; set; } + public int Level { get; set; } +} diff --git a/src/Core/Player/CurrentAccountState.cs b/src/Core/Player/CurrentAccountState.cs index d61e27b..7779a6f 100644 --- a/src/Core/Player/CurrentAccountState.cs +++ b/src/Core/Player/CurrentAccountState.cs @@ -1,6 +1,4 @@ -using Com.Ankama.Dofus.Server.Connection.Protocol; - -namespace DofusBatteriesIncluded.Plugins.Core.Player; +namespace DofusBatteriesIncluded.Plugins.Core.Player; public class CurrentAccountState { diff --git a/src/Core/Player/CurrentPlayerState.cs b/src/Core/Player/CurrentPlayerState.cs index ac8cc02..3181ccb 100644 --- a/src/Core/Player/CurrentPlayerState.cs +++ b/src/Core/Player/CurrentPlayerState.cs @@ -1,5 +1,4 @@ using System; -using Com.Ankama.Dofus.Server.Game.Protocol.Common; using Core.DataCenter; using Core.DataCenter.Metadata.World; using DofusBatteriesIncluded.Plugins.Core.Maps; @@ -14,8 +13,8 @@ public class CurrentPlayerState public CurrentPlayerState(Character character) { CharacterId = character.Id; - Name = character.CharacterBasicInformation.Name; - Level = character.CharacterBasicInformation.Level; + Name = character.Name; + Level = character.Level; } public long CharacterId { get; } diff --git a/src/Core/Player/UpdateCurrentAccount.cs b/src/Core/Player/UpdateCurrentAccount.cs index 87c74cb..f1ef0f1 100644 --- a/src/Core/Player/UpdateCurrentAccount.cs +++ b/src/Core/Player/UpdateCurrentAccount.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Com.Ankama.Dofus.Server.Connection.Protocol; using DofusBatteriesIncluded.Plugins.Core.Protocol; namespace DofusBatteriesIncluded.Plugins.Core.Player; diff --git a/src/Core/Player/UpdateCurrentPlayer.cs b/src/Core/Player/UpdateCurrentPlayer.cs index e89927e..ba4631c 100644 --- a/src/Core/Player/UpdateCurrentPlayer.cs +++ b/src/Core/Player/UpdateCurrentPlayer.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Com.Ankama.Dofus.Server.Game.Protocol.Character.Management; using DofusBatteriesIncluded.Plugins.Core.Protocol; namespace DofusBatteriesIncluded.Plugins.Core.Player; @@ -23,3 +22,26 @@ public Task HandleAsync(CharacterLoadingCompleteEvent message) return Task.CompletedTask; } } + +public class CharacterLoadingCompleteEvent +{ +} + +public class CharacterSelectionEvent +{ + public ResultOneofCase ResultCase { get; set; } + public Types.Success Success { get; set; } + + public enum ResultOneofCase + { + Success + } + + public class Types + { + public class Success + { + public Character Character { get; set; } + } + } +} diff --git a/src/Core/Player/UpdateCurrentPlayerMap.cs b/src/Core/Player/UpdateCurrentPlayerMap.cs index e6fd67d..8ee986a 100644 --- a/src/Core/Player/UpdateCurrentPlayerMap.cs +++ b/src/Core/Player/UpdateCurrentPlayerMap.cs @@ -1,6 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using Com.Ankama.Dofus.Server.Game.Protocol.Gamemap; using DofusBatteriesIncluded.Plugins.Core.Protocol; namespace DofusBatteriesIncluded.Plugins.Core.Player; @@ -26,7 +26,7 @@ public Task HandleAsync(MapMovementEvent message) return Task.CompletedTask; } - DBI.Player.CurrentCharacter.SetCurrentCellId(message.Cells.array.Last(c => c != default)); + DBI.Player.CurrentCharacter.SetCurrentCellId(message.Cells.Last(c => c != default)); return Task.CompletedTask; } @@ -34,3 +34,14 @@ public Task HandleAsync(MapMovementEvent message) static bool IsMessageForCurrentPlayer(MapCurrentEvent message) => DBI.Player.CurrentCharacter != null; static bool IsMessageForCurrentPlayer(MapMovementEvent message) => DBI.Player.CurrentCharacter != null && DBI.Player.CurrentCharacter.CharacterId == message.CharacterId; } + +public class MapMovementEvent +{ + public IReadOnlyList Cells { get; set; } + public long CharacterId { get; set; } +} + +public class MapCurrentEvent +{ + public long MapId { get; set; } +} diff --git a/src/Core/Protocol/Messaging.cs b/src/Core/Protocol/Messaging.cs index 2e0722f..0baba5c 100644 --- a/src/Core/Protocol/Messaging.cs +++ b/src/Core/Protocol/Messaging.cs @@ -2,13 +2,11 @@ using System.Linq; using System.Reflection; using System.Text.Json; -using Com.Ankama.Dofus.Server.Connection.Protocol; -using Com.Ankama.Dofus.Server.Game.Protocol.Treasure.Hunt; +using DofusBatteriesIncluded.Plugins.Core.Deobfuscation.Protocol; using Google.Protobuf; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Microsoft.Extensions.Logging; -using Message = Com.Ankama.Dofus.Server.Game.Protocol.Message; namespace DofusBatteriesIncluded.Plugins.Core.Protocol; @@ -18,15 +16,18 @@ public static class Messaging public static event EventHandler MessageReceived; + static readonly ObfuscatedMessageReader MessageReader = new(); + static readonly ObfuscatedResponseReader ResponseReader = new(); + [HarmonyPatch(typeof(MessageParser), nameof(MessageParser.ParseFrom), typeof(CodedInputStream))] [HarmonyPostfix] static void Patch(IMessage __result) { - Message message = new(__result.Pointer); + ObfuscatedMessage message = MessageReader.GetMessage(__result); - switch (message.ContentCase) + switch (message.ContentOneOfCase) { - case Message.ContentOneofCase.Event: + case ObfuscatedMessageContentOneOfCase.Event: { string json = message.ToString(); JsonDocument obj = JsonDocument.Parse(json); @@ -41,13 +42,13 @@ static void Patch(IMessage __result) } break; } - case Message.ContentOneofCase.Response: + case ObfuscatedMessageContentOneOfCase.Response: { HandleResponseMessage(message); break; } - case Message.ContentOneofCase.None: - case Message.ContentOneofCase.Request: + case ObfuscatedMessageContentOneOfCase.None: + case ObfuscatedMessageContentOneOfCase.Request: default: { string json = message.ToString(); @@ -124,38 +125,60 @@ static Type FindEventType(string typeName) return null; } - static void HandleResponseMessage(Message message) + static void HandleResponseMessage(ObfuscatedMessage message) { - Response response = new(message.content_.Pointer); + IntPtr? pointer = message.ContentPointer; + if (!pointer.HasValue) + { + Log.LogError("Could not find content of response message.\n{Response}.", message); + return; + } + + ObfuscatedResponse response = ResponseReader.GetResponse(pointer.Value); + if (response == null) + { + Log.LogError("Could not read response.\n{Response}", message); + return; + } object content; - switch (response.ContentCase) + switch (response.GetContentOneOfCaseInResponse) { - case Response.ContentOneofCase.Identification: + case ObfuscatedResponseContentOneOfCase.Identification: content = response.Identification; break; - case Response.ContentOneofCase.Pong: + case ObfuscatedResponseContentOneOfCase.Pong: content = response.Pong; break; - case Response.ContentOneofCase.SelectServer: + case ObfuscatedResponseContentOneOfCase.SelectServer: content = response.SelectServer; break; - case Response.ContentOneofCase.ForceAccount: + case ObfuscatedResponseContentOneOfCase.ForceAccount: content = response.ForceAccount; break; - case Response.ContentOneofCase.FriendList: + case ObfuscatedResponseContentOneOfCase.FriendList: content = response.FriendList; break; - case Response.ContentOneofCase.AcquaintanceServersResponse: + case ObfuscatedResponseContentOneOfCase.AcquaintanceServersResponse: content = response.AcquaintanceServersResponse; break; - case Response.ContentOneofCase.None: + case ObfuscatedResponseContentOneOfCase.None: default: Log.LogError("Could not find type of response."); return; } + if (content == null) + { + Log.LogError("Response content is null.\n{Response}.", response); + return; + } + Log.LogInformation("Received response of type {Type}.", content.GetType()); MessageReceived?.Invoke(null, content); } } + +class TreasureHuntEvent +{ +}