diff --git a/Library/RSBot.Core/Components/ClientManager.cs b/Library/RSBot.Core/Components/ClientManager.cs
index 3f87181a..d3692061 100644
--- a/Library/RSBot.Core/Components/ClientManager.cs
+++ b/Library/RSBot.Core/Components/ClientManager.cs
@@ -14,6 +14,15 @@ namespace RSBot.Core.Components;
public class ClientManager
{
+ private const int SroClientImageBase = 0x00400000;
+ private static readonly (int Address, byte[] Expected, byte[] Patch)[] ChineseClientPatches =
+ {
+ (0x0060C6DB, new byte[] { 0x75, 0x2E }, new byte[] { 0xEB, 0x2E }),
+ (0x0060C6A1, new byte[] { 0x75, 0x26 }, new byte[] { 0xEB, 0x26 }),
+ (0x0067BF56, new byte[] { 0x75, 0x2C }, new byte[] { 0xEB, 0x2C }),
+ (0x00AD449E, new byte[] { 0x68, 0x5C, 0x54, 0xE1, 0x00 }, new byte[] { 0xEB, 0x2D, 0x90, 0x90, 0x90 }),
+ };
+
///
/// The client process
///
@@ -153,6 +162,13 @@ out var pi
WaitForSingleObject(remoteThread, uint.MaxValue);
+ if (Game.ClientType == GameClientType.Chinese)
+ {
+ var sroProcess = System.Diagnostics.Process.GetProcessById((int)pi.dwProcessId);
+ WaitForChineseClientPatchTargets(sroProcess, pi.hThread);
+ ApplyChineseClientPatch(sroProcess, pi);
+ }
+
VirtualFreeEx(handle, remotePath, 0, MEM_RELEASE);
CloseHandle(remoteThread);
@@ -190,10 +206,6 @@ public static void BypassLauncherCheck(Process process, PROCESS_INFORMATION pi)
out _
);
- var patchNop = new byte[] { 0x90, 0x90 };
- var patchNop2 = new byte[] { 0x90, 0x90, 0x90, 0x90, 0x90 };
- var patchJmp = new byte[] { 0xEB };
-
string signature =
"55 8B EC 83 EC ?? 8B 45 ?? 50 E8 ?? ?? ?? ?? 83 C4 04 89 45 ?? 8B 4D ?? 89 4D ?? 68 ?? ?? ?? ?? 6A 00 6A 00";
@@ -367,6 +379,130 @@ out _
GC.Collect();
}
+ ///
+ /// Runs the suspended Chinese client until the target bytes are unpacked and readable.
+ /// On return the primary thread is left suspended so the caller can patch and resume.
+ ///
+ private static void WaitForChineseClientPatchTargets(Process process, IntPtr hThread)
+ {
+ const int sliceMs = 8;
+ const int maxWaitMs = 5000;
+
+ var sw = Stopwatch.StartNew();
+
+ while (true)
+ {
+ ResumeThread(hThread);
+ Thread.Sleep(sliceMs);
+ SuspendThread(hThread);
+
+ if (process.HasExited)
+ return;
+
+ process.Refresh();
+
+ if (ChineseClientPatchTargetsReady(process.Handle, process.MainModule.BaseAddress.ToInt32()))
+ return;
+
+ if (sw.ElapsedMilliseconds >= maxWaitMs)
+ {
+ Log.Warn("Chinese client patch: unpack timeout.");
+ return;
+ }
+ }
+ }
+
+ private static bool ChineseClientPatchTargetsReady(IntPtr processHandle, int moduleBaseAddress)
+ {
+ foreach (var patch in ChineseClientPatches)
+ {
+ if (
+ !TryReadClientBytes(
+ processHandle,
+ moduleBaseAddress,
+ patch.Address,
+ patch.Expected.Length,
+ out var current
+ )
+ )
+ return false;
+
+ if (!current.SequenceEqual(patch.Expected) && !current.SequenceEqual(patch.Patch))
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Applies fixed in-memory patches for the Chinese sro_client.exe.
+ ///
+ private static void ApplyChineseClientPatch(Process process, PROCESS_INFORMATION pi)
+ {
+ int moduleBaseAddress = process.MainModule.BaseAddress.ToInt32();
+
+ foreach (var patch in ChineseClientPatches)
+ PatchKnownClientBytes(pi.hProcess, moduleBaseAddress, patch.Address, patch.Expected, patch.Patch);
+ }
+
+ private static void PatchKnownClientBytes(
+ IntPtr processHandle,
+ int moduleBaseAddress,
+ int clientAddress,
+ byte[] expected,
+ byte[] patch
+ )
+ {
+ if (!TryReadClientBytes(processHandle, moduleBaseAddress, clientAddress, expected.Length, out var current))
+ {
+ Log.Warn($"Chinese client patch: could not read 0x{clientAddress:X8}.");
+ return;
+ }
+
+ if (current.SequenceEqual(patch))
+ return;
+
+ if (!current.SequenceEqual(expected))
+ {
+ Log.Warn(
+ $"Chinese client patch: unexpected bytes at 0x{clientAddress:X8}. Expected {FormatBytes(expected)}, got {FormatBytes(current)}."
+ );
+ return;
+ }
+
+ PatchProcessMemory(processHandle, GetClientAddress(moduleBaseAddress, clientAddress), patch);
+ }
+
+ private static bool TryReadClientBytes(
+ IntPtr processHandle,
+ int moduleBaseAddress,
+ int clientAddress,
+ int length,
+ out byte[] bytes
+ )
+ {
+ bytes = new byte[length];
+
+ return ReadProcessMemory(
+ processHandle,
+ GetClientAddress(moduleBaseAddress, clientAddress),
+ bytes,
+ bytes.Length,
+ out var read
+ )
+ && read.ToInt32() == bytes.Length;
+ }
+
+ private static IntPtr GetClientAddress(int moduleBaseAddress, int clientAddress)
+ {
+ return (IntPtr)(moduleBaseAddress + clientAddress - SroClientImageBase);
+ }
+
+ private static string FormatBytes(byte[] bytes)
+ {
+ return BitConverter.ToString(bytes).Replace("-", " ");
+ }
+
///
/// Finds the XIGNCODE init call anchor in WinMain by locating the standalone "XIGNCODE" Unicode
/// string and its reference in the code pattern: push 0; push VA("XIGNCODE"); push code_string; call func.
diff --git a/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs b/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs
index 729cdc4e..78f60a45 100644
--- a/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs
+++ b/Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using RSBot.Core.Event;
using RSBot.Core.Objects;
using RSBot.Core.Objects.Item;
@@ -30,14 +30,17 @@ internal class InventoryUpdateItemResponse : IPacketHandler
public void Invoke(Packet packet)
{
if (Game.ClientType == GameClientType.Global || Game.ClientType == GameClientType.RuSro)
- if (packet.ReadByte() != 0) //sometimes appears 9 with unknown structure
- return;
-
- var sourceSlot = packet.ReadByte();
+ {
+ InvokeModernClients(packet);
+ return;
+ }
- if (Game.ClientType == GameClientType.Global || Game.ClientType == GameClientType.RuSro)
- packet.ReadByte(); //0 - normal, 2 - item disappearing
+ InvokeLegacy(packet);
+ }
+ private static void InvokeLegacy(Packet packet)
+ {
+ var sourceSlot = packet.ReadByte();
var itemUpdateFlag = (ItemUpdateFlag)packet.ReadByte();
var item = Game.Player.Inventory.GetItemAt(sourceSlot);
@@ -79,4 +82,136 @@ public void Invoke(Packet packet)
EventManager.FireEvent("OnUpdateInventoryItem", sourceSlot);
}
+
+ private static void InvokeModernClients(Packet packet)
+ {
+ var updateType = packet.ReadByte();
+
+ if (updateType == 9)
+ {
+ var objectId = packet.ReadUInt();
+ var containerSlot = packet.ReadByte();
+ var updateFlags = (ModernItemUpdateFlag)packet.ReadUShort();
+ var inventory = GetInventoryForObject(objectId);
+ var containerItem = inventory?.GetItemAt(containerSlot);
+
+ ApplyModernClientsContainerUpdate(packet, containerItem, updateFlags);
+
+ if (containerItem != null && ReferenceEquals(inventory, Game.Player.Inventory))
+ EventManager.FireEvent("OnUpdateInventoryItem", containerSlot);
+
+ return;
+ }
+
+ var sourceSlot = packet.ReadByte();
+ var itemUpdateFlags = (ModernItemUpdateFlag)packet.ReadUShort();
+ var item = Game.Player.Inventory.GetItemAt(sourceSlot);
+ if (item == null)
+ return;
+
+ ApplyModernClientsItemUpdate(packet, item, sourceSlot, itemUpdateFlags, updateType);
+ EventManager.FireEvent("OnUpdateInventoryItem", sourceSlot);
+ }
+
+ private static void ApplyModernClientsContainerUpdate(
+ Packet packet,
+ InventoryItem item,
+ ModernItemUpdateFlag updateFlags
+ )
+ {
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.Durability))
+ {
+ var durability = packet.ReadUInt();
+ if (item != null)
+ item.Durability = durability;
+ }
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.BindingOptions))
+ ReadBindingOptions(packet, item);
+ }
+
+ private static void ApplyModernClientsItemUpdate(
+ Packet packet,
+ InventoryItem item,
+ byte sourceSlot,
+ ModernItemUpdateFlag updateFlags,
+ byte updateType
+ )
+ {
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.RefObjID))
+ item.ItemId = packet.ReadUInt();
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.Quantity))
+ item.Amount = packet.ReadUShort();
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.Durability))
+ item.Durability = packet.ReadUInt();
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.OptLevel))
+ item.OptLevel = packet.ReadByte();
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.Variance))
+ item.Attributes = new ItemAttributesInfo(packet.ReadULong());
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams) && item.Record != null)
+ ReadMagicOptions(packet, item);
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.BindingOptions) && (updateType == 0 || updateType == 8))
+ ReadBindingOptions(packet, item);
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.RemainTime))
+ packet.ReadUInt();
+
+ if (updateFlags.HasFlag(ModernItemUpdateFlag.ItemState))
+ {
+ var itemState = (InventoryItemState)packet.ReadByte();
+
+ if (sourceSlot >= 0x11)
+ item.State = itemState;
+ }
+ }
+
+ private static void ReadMagicOptions(Packet packet, InventoryItem item)
+ {
+ item.MagicOptions = new List();
+
+ var magParamCount = packet.ReadByte();
+ for (var i = 0; i < magParamCount; i++)
+ item.MagicOptions.Add(MagicOptionInfo.FromPacket(packet));
+ }
+
+ private static void ReadBindingOptions(Packet packet, InventoryItem item)
+ {
+ item ??= new InventoryItem();
+ item.BindingOptions = new List();
+
+ for (var bindingIndex = 0; bindingIndex < 4; bindingIndex++)
+ {
+ var bindingType = (BindingOptionType)packet.ReadByte();
+ var bindingAmount = packet.ReadByte();
+
+ for (var i = 0; i < bindingAmount; i++)
+ item.BindingOptions.Add(BindingOption.FromPacket(packet, bindingType));
+ }
+ }
+
+ private static InventoryItemCollection GetInventoryForObject(uint objectId)
+ {
+ if (Game.Player.UniqueId == objectId)
+ return Game.Player.Inventory;
+
+ if (Game.Player.AbilityPet?.UniqueId == objectId)
+ return Game.Player.AbilityPet.Inventory;
+
+ if (Game.Player.Fellow?.UniqueId == objectId)
+ return Game.Player.Fellow.Inventory;
+
+ if (Game.Player.Growth?.UniqueId == objectId)
+ return Game.Player.Growth.Inventory;
+
+ if (Game.Player.Vehicle?.UniqueId == objectId)
+ return Game.Player.Vehicle.Inventory;
+
+ return null;
+ }
}
diff --git a/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs b/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs
new file mode 100644
index 00000000..8b03a83a
--- /dev/null
+++ b/Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace RSBot.Core.Objects.Item;
+
+[Flags]
+public enum ModernItemUpdateFlag : ushort
+{
+ RefObjID = 0x0001,
+ Quantity = 0x0002,
+ Durability = 0x0010,
+ OptLevel = 0x0020,
+ Variance = 0x0040,
+ MagParams = 0x0080,
+ BindingOptions = 0x0100,
+ RemainTime = 0x0200,
+ ItemState = 0x8000,
+}
diff --git a/Plugins/RSBot.General/Components/AutoLogin.cs b/Plugins/RSBot.General/Components/AutoLogin.cs
index 5e76661e..5c0f567f 100644
--- a/Plugins/RSBot.General/Components/AutoLogin.cs
+++ b/Plugins/RSBot.General/Components/AutoLogin.cs
@@ -191,7 +191,15 @@ private static void SendLoginRequest(Account account, Server server)
loginPacket.WriteUShort(server.Id);
- if (opcode == 0x610A)
+ if (Game.ClientType == GameClientType.Chinese)
+ {
+ if (!ChineseGatewayLogin.TryWriteVerificationAndTicket(loginPacket, account.Username))
+ {
+ _busy = false;
+ return;
+ }
+ }
+ else if (opcode == 0x610A)
loginPacket.WriteByte(account.Channel);
PacketManager.SendPacket(loginPacket, PacketDestination.Server);
diff --git a/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs b/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs
new file mode 100644
index 00000000..71953aae
--- /dev/null
+++ b/Plugins/RSBot.General/Components/ChineseGatewayLogin.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+using RSBot.Core;
+using RSBot.Core.Network;
+
+namespace RSBot.General.Components;
+
+internal static class ChineseGatewayLogin
+{
+ private const int MachineTokenKeySeed = 0x0118860C;
+ private const string TicketKey = "UGCKernel";
+ private const int MaxSilpsetLength = 4096;
+
+ private static readonly byte[] TicketMarker = { 0x11, 0x00, 0x25, 0x56, 0x00, 0x33 };
+ private static readonly Lazy TicketSeed = new(CreateTicketSeed);
+
+ public static bool TryWriteVerificationAndTicket(Packet packet, string username)
+ {
+ try
+ {
+ var ticket = BuildTicket();
+
+ packet.WriteUInt(ComputeVerifyResult(username));
+ packet.WriteUInt((uint)ticket.Length);
+ packet.WriteBytes(ticket);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Chinese gateway ticket could not be created: {ex.Message}");
+ return false;
+ }
+ }
+
+ private static byte[] BuildTicket()
+ {
+ var machineToken = BuildMachineToken();
+ var silpsetToken = ReadSilpsetToken();
+
+ if (machineToken.Length > ushort.MaxValue || silpsetToken.Length > ushort.MaxValue)
+ throw new InvalidOperationException("Chinese gateway ticket token is too large.");
+
+ using MemoryStream stream = new();
+ using BinaryWriter writer = new(stream);
+
+ writer.Write((long)(int)TicketSeed.Value);
+ writer.Write(TicketMarker);
+ writer.Write((ushort)machineToken.Length);
+ writer.Write(machineToken);
+ writer.Write((ushort)silpsetToken.Length);
+ writer.Write(silpsetToken);
+
+ var ticket = stream.ToArray();
+ Rc4Xor(ticket, Encoding.ASCII.GetBytes(TicketKey));
+
+ return ticket;
+ }
+
+ private static byte[] BuildMachineToken()
+ {
+ string hostName;
+ try
+ {
+ hostName = Dns.GetHostName();
+ }
+ catch
+ {
+ hostName = Environment.MachineName;
+ if (hostName.Length > 16)
+ hostName = hostName.Substring(0, 16);
+ }
+
+ if (string.IsNullOrEmpty(hostName))
+ throw new InvalidOperationException("Computer name is empty.");
+
+ if (hostName.Length > byte.MaxValue)
+ hostName = hostName.Substring(0, byte.MaxValue);
+
+ var token = Encoding.ASCII.GetBytes(hostName);
+ var key = Encoding.ASCII.GetBytes(MachineTokenKeySeed.ToString(CultureInfo.InvariantCulture));
+ Rc4Xor(token, key);
+
+ return token;
+ }
+
+ private static byte[] ReadSilpsetToken()
+ {
+ var silkroadDirectory = GlobalConfig.Get("RSBot.SilkroadDirectory");
+ if (string.IsNullOrWhiteSpace(silkroadDirectory))
+ throw new InvalidOperationException("Silkroad directory is not configured.");
+
+ var path = Path.Combine(silkroadDirectory, "setting", "SRsilpset.dat");
+ if (!File.Exists(path))
+ throw new FileNotFoundException("SRsilpset.dat was not found.", path);
+
+ var bytes = File.ReadAllBytes(path);
+ if (bytes.Length > MaxSilpsetLength)
+ throw new InvalidOperationException("SRsilpset.dat is larger than the client buffer.");
+
+ return bytes;
+ }
+
+ private static uint ComputeVerifyResult(string username)
+ {
+ if (string.IsNullOrEmpty(username))
+ return 0;
+
+ uint crc = 0xFFFFFFFF;
+ var bytes = Encoding.GetEncoding(950).GetBytes(username);
+
+ foreach (var value in bytes)
+ {
+ crc ^= value;
+
+ for (var bit = 0; bit < 8; bit++)
+ crc = (crc & 1) == 0 ? crc >> 1 : (crc >> 1) ^ 0xEDB88320;
+ }
+
+ return ~crc;
+ }
+
+ private static uint CreateTicketSeed()
+ {
+ if (CoCreateGuid(out var guid) != 0)
+ guid = Guid.NewGuid();
+
+ var bytes = guid.ToByteArray();
+ return BitConverter.ToUInt32(bytes, 0) + BitConverter.ToUInt16(bytes, 4) + BitConverter.ToUInt16(bytes, 6);
+ }
+
+ private static void Rc4Xor(byte[] buffer, byte[] key)
+ {
+ var state = new byte[256];
+ for (var i = 0; i < state.Length; i++)
+ state[i] = (byte)i;
+
+ var j = 0;
+ for (var i = 0; i < state.Length; i++)
+ {
+ j = (j + state[i] + key[i % key.Length]) & 0xFF;
+ (state[i], state[j]) = (state[j], state[i]);
+ }
+
+ var x = 0;
+ j = 0;
+ for (var index = 0; index < buffer.Length; index++)
+ {
+ x = (x + 1) & 0xFF;
+ j = (j + state[x]) & 0xFF;
+ (state[x], state[j]) = (state[j], state[x]);
+
+ buffer[index] ^= state[(state[x] + state[j]) & 0xFF];
+ }
+ }
+
+ [DllImport("ole32.dll")]
+ private static extern int CoCreateGuid(out Guid guid);
+}
diff --git a/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs b/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs
index 32727d31..67c92945 100644
--- a/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs
+++ b/Plugins/RSBot.General/PacketHandler/GatewayLoginRequest.cs
@@ -44,7 +44,18 @@ public void Invoke(Packet packet)
var shardId = packet.ReadUShort();
- if (Game.ClientType >= GameClientType.Chinese)
+ if (Game.ClientType == GameClientType.Chinese)
+ {
+ if (packet.Remaining >= 8)
+ {
+ packet.ReadUInt(); // verify_result
+ var ticketLength = packet.ReadUInt();
+
+ if (ticketLength <= packet.Remaining)
+ packet.ReadBytes((int)ticketLength); // ticket
+ }
+ }
+ else if (Game.ClientType >= GameClientType.Chinese)
packet.ReadByte(); // channel
Serverlist.SetJoining(shardId);
diff --git a/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs b/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs
index a13ea943..d9c0e993 100644
--- a/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs
+++ b/Plugins/RSBot.General/PacketHandler/GatewayServerListResponse.cs
@@ -58,7 +58,7 @@ public void Invoke(Packet packet)
byte status;
- if (Game.ClientType < GameClientType.Global)
+ if (Game.ClientType < GameClientType.Chinese)
{
currentCapacity = packet.ReadUShort();
maxCapacity = packet.ReadUShort();
@@ -74,6 +74,9 @@ public void Invoke(Packet packet)
if (Game.ClientType == GameClientType.Global)
serverName = serverName.Remove(0, 1);
+ if (Game.ClientType == GameClientType.Chinese && serverName.EndsWith("#$C"))
+ serverName = serverName.Remove(serverName.Length - 3);
+
if (Game.ClientType == GameClientType.VTC_Game)
if (serverName.EndsWith("Thien_Kim"))
serverName = serverName.Remove(0, 3);