Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
144 changes: 140 additions & 4 deletions Library/RSBot.Core/Components/ClientManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
};

/// <summary>
/// The client process
/// </summary>
Expand Down Expand Up @@ -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);
}
Comment on lines +165 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make Chinese patch readiness a hard startup gate (and handle module-read races).

Line 169-174 always continues startup even when Line 411-415 times out, so Chinese clients can resume unpatched. Also, Line 406-409 can throw while module metadata is transiently unreadable during unpack, which can abort startup.

Suggested fix
-                WaitForChineseClientPatchTargets(sroProcess, pi.hThread);
+                if (!WaitForChineseClientPatchTargets(sroProcess, pi.hThread))
+                {
+                    Log.Error("Chinese client patch: targets were not ready before timeout.");
+                    return false;
+                }
                 ApplyChineseClientPatch(sroProcess, pi);
...
-    private static void WaitForChineseClientPatchTargets(Process process, IntPtr hThread)
+    private static bool WaitForChineseClientPatchTargets(Process process, IntPtr hThread)
     {
         const int sliceMs = 8;
         const int maxWaitMs = 5000;
...
-            if (process.HasExited)
-                return;
-
-            process.Refresh();
-
-            if (ChineseClientPatchTargetsReady(process.Handle, process.MainModule.BaseAddress.ToInt32()))
-                return;
+            if (process.HasExited)
+                return false;
+
+            try
+            {
+                process.Refresh();
+                if (ChineseClientPatchTargetsReady(process.Handle, process.MainModule.BaseAddress.ToInt32()))
+                    return true;
+            }
+            catch
+            {
+                // Module can be temporarily unreadable while unpacking; keep polling.
+            }
...
             if (sw.ElapsedMilliseconds >= maxWaitMs)
             {
                 Log.Warn("Chinese client patch: unpack timeout.");
-                return;
+                return false;
             }
         }
     }

Also applies to: 390-417

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Library/RSBot.Core/Components/ClientManager.cs` around lines 169 - 174, The
startup currently proceeds past the Chinese-client patch window causing
unpatched clients; modify ClientManager startup so that when Game.ClientType ==
GameClientType.Chinese the code blocks startup until
WaitForChineseClientPatchTargets succeeds and ApplyChineseClientPatch completes,
and if WaitForChineseClientPatchTargets times out or ApplyChineseClientPatch
fails, abort startup (throw or stop further initialization) rather than
continuing. Also wrap any transient module metadata reads used by
WaitForChineseClientPatchTargets (the module enumeration / metadata access it
performs) in a short retry loop with small delays and a bounded overall timeout
to handle module-read races during unpacking, surfacing the final failure to the
startup gate so the process won’t continue with an unpatched client.


VirtualFreeEx(handle, remotePath, 0, MEM_RELEASE);

CloseHandle(remoteThread);
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -367,6 +379,130 @@ out _
GC.Collect();
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// Applies fixed in-memory patches for the Chinese sro_client.exe.
/// </summary>
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("-", " ");
}

/// <summary>
/// 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Comment on lines +156 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always consume MagParams when the flag is set.

Line 155 makes packet consumption depend on item.Record != null, but the magic-option payload is still present on the wire. If that guard fails, every later flagged read in this update shifts onto the wrong offset.

Suggested fix
-        if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams) && item.Record != null)
+        if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams))
             ReadMagicOptions(packet, item);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams) && item.Record != null)
ReadMagicOptions(packet, item);
if (updateFlags.HasFlag(ModernItemUpdateFlag.MagParams))
ReadMagicOptions(packet, item);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Library/RSBot.Core/Network/Handler/Agent/Inventory/InventoryUpdateItemResponse.cs`
around lines 155 - 156, The packet parsing currently skips consuming the
MagParams payload when ModernItemUpdateFlag.MagParams is set but item.Record is
null, causing subsequent reads to be misaligned; remove the item.Record guard so
that when updateFlags.HasFlag(ModernItemUpdateFlag.MagParams) is true you always
call ReadMagicOptions(packet, item) (or call a null-safe variant of
ReadMagicOptions that consumes the fields even if item.Record is null) to ensure
the wire payload is always consumed and parsing stays in sync.


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<MagicOptionInfo>();

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<BindingOption>();

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;
}
}
17 changes: 17 additions & 0 deletions Library/RSBot.Core/Objects/Inventory/Item/ModernItemUpdateFlag.cs
Original file line number Diff line number Diff line change
@@ -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,
}
10 changes: 9 additions & 1 deletion Plugins/RSBot.General/Components/AutoLogin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading