Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WoWLib.PacketCapture;
namespace Sniffer.Net
{
class Program
{
static void Main(string[] args)
{
// this may not work on your machine -- instead, use some sane way of picking
// the right cap device from AllDevices (look at the Description to figure out
// what's what)
var wpd = ClientConnectionManager.AllDevices.First();
ClientConnectionManager ccm = new ClientConnectionManager(wpd);
ccm.ClientConnected += new ClientConnectionEstablishedDelegate(ccm_ClientConnected);
ccm.ClientConnectionCreated += new ClientConnectionCreatedDelegate(ccm_ClientConnectionCreated);
ccm.ClientDisconnected += new ClientConnectionDisconnectedDelegate(ccm_ClientDisconnected);
ccm.StartedCapturing += new StartedCapturingDelegate(ccm_StartedCapturing);
ccm.StoppedCapturing += new StoppedCapturingDelegate(ccm_StoppedCapturing);
ccm.Run();
while (true)
{
}
}
static void ccm_StoppedCapturing(IClientConnectionManager manager)
{
Console.WriteLine("Stopped capturing on {0}", manager.Description);
}
static void ccm_StartedCapturing(IClientConnectionManager manager)
{
Console.WriteLine("Started capturing on {0}", manager.Description);
}
static void ccm_ClientDisconnected(IClientConnectionManager manager, IClientConnectionInfo connection, MessageDirection initiator)
{
Console.WriteLine("Client id {0} disconnected on {1}", connection.Id, manager.Description);
}
static void ccm_ClientConnectionCreated(IClientConnectionManager manager, IClientConnectionInfo connection)
{
Console.WriteLine("Client connection id {0} (remote addr: {1}) created on {2}", connection.Id, connection.RemoteAddress.ToString(), manager.Description);
}
static void ccm_ClientConnected(IClientConnectionManager manager, IClientConnectionInfo connection, MessageDirection initiator)
{
Console.WriteLine("Client connection id {0} (remote addr: {1}) connected on {2}", connection.Id, connection.RemoteAddress.ToString(), manager.Description);
connection.LogicalPacketDecoded += new LogicalPacketDecodedDelegate(connection_LogicalPacketDecoded);
}
static void connection_LogicalPacketDecoded(IClientConnectionInfo connection, LogicalPacket packet)
{
Console.WriteLine("[{0} conn {1}] {2}_{3}",
DateTime.Now,
connection.Id,
packet.Direction.ToString(),
packet.Opcode.ToString());
}
}
}
Here's (arguably) the most important opcode parser -- SMSG_UPDATE_OBJECT. This (obviously) won't compile, since you'd need the rest of my GameContext code (the object that abstracts away one connection's "view" of the game world), but it should help anyone interested get started parsing these critical packets. The original was ripped from the sniffitzt sources, but keeping it up-to-date is nontrivial.
Code:
using System;
using System.Linq;
using WoWLib.Logging;
using WoWLib.Constants;
using WoWLib.PacketCapture;
namespace WoWLib.GameStateManager
{
public enum InstancedObjectType : byte
{
TYPEID_OBJECT=0,
TYPEID_ITEM=1,
TYPEID_CONTAINER=2,
TYPEID_UNIT=3,
TYPEID_PLAYER=4,
TYPEID_GAMEOBJECT=5,
TYPEID_DYNAMICOBJECT=6,
TYPEID_CORPSE=7,
TYPEID_AIGROUP=8,
TYPEID_AREATRIGGER=9
}
public enum ObjectUpdateType : byte
{
UPDATETYPE_VALUES = 0,
UPDATETYPE_MOVEMENT = 1,
UPDATETYPE_CREATE_OBJECT = 2,
UPDATETYPE_CREATE_OBJECT2 = 3,
UPDATETYPE_OUT_OF_RANGE_OBJECTS = 4,
UPDATETYPE_NEAR_OBJECTS = 5
}
[Opcode(Opcodes.SMSG_DESTROY_OBJECT, MessageDirection.StoC)]
public class S2C_SMSG_DESTROY_OBJECT : IOpcodeHandler
{
public void Decode(GameContext ctx, LogicalPacket packet)
{
var buffer = new ProcessByteBuffer(packet.Buffer);
ulong guid = buffer.ReadWoWGuid();
System.Diagnostics.Debug.Assert(guid != ctx.LocalPlayer.guid);
ctx.Data.RemoveObjectByGuid(guid);
}
}
[Opcode(Opcodes.SMSG_UPDATE_OBJECT, MessageDirection.StoC)]
public class S2C_SMSG_UPDATE_OBJECT : IOpcodeHandler
{
public void Decode(GameContext ctx, LogicalPacket packet)
{
ProcessByteBuffer buffer = new ProcessByteBuffer(packet.Buffer);
uint blockcount = buffer.Read<uint>();
for (int i = 0; i < blockcount; i++)
{
ObjectUpdateType type = (ObjectUpdateType)buffer.Read<byte>();
switch (type)
{
case ObjectUpdateType.UPDATETYPE_VALUES:
{
InstancedObject obj = ctx.Data.GetObjectByGuid(buffer.ReadPackedGuid());
UpdateObjectFields(obj, ctx, buffer, packet.Timestamp);
break;
}
case ObjectUpdateType.UPDATETYPE_MOVEMENT:
{
InstancedObject obj = ctx.Data.GetObjectByGuid(buffer.ReadPackedGuid());
UpdateObjectPosition(obj, ctx, buffer, packet.Timestamp);
break;
}
case ObjectUpdateType.UPDATETYPE_CREATE_OBJECT2:
case ObjectUpdateType.UPDATETYPE_CREATE_OBJECT:
{
CreateObject(ctx, buffer, packet.Timestamp);
break;
}
case ObjectUpdateType.UPDATETYPE_NEAR_OBJECTS:
{
long nsize = buffer.ReadUInt32();
for (long j = 0; j < nsize; j++)
{
ulong guid = buffer.ReadPackedGuid();
// what do we do with it?
}
break;
}
case ObjectUpdateType.UPDATETYPE_OUT_OF_RANGE_OBJECTS:
{
uint size = buffer.Read<uint>();
for (long j = 0; j < size; j++)
{
ulong guid = buffer.ReadPackedGuid();
System.Diagnostics.Debug.Assert(guid != ctx.LocalPlayer.guid); // the local player is out of range??
ctx.Data.RemoveObjectByGuid(guid);
}
break;
}
default:
throw new ApplicationException("Error: unknown updatetype " + type);
}
}
}
private InstancedObject CreateObject(GameContext ctx, ProcessByteBuffer buffer, DateTime timeStamp)
{
ulong guid = buffer.ReadPackedGuid();
WoWObjectType objectType = (WoWObjectType)buffer.ReadByte();
InstancedObject existing = ctx.Data.GetObjectByGuid(guid);
if (existing != null)
{
Logger.Log(Level.Warning, "Warning, creating already-existing object guid {0:x16}, type {1}", guid, objectType);
}
else
{
existing = ctx.Data.GetOrInitializeObject(guid, objectType, timeStamp);
}
UpdateObjectPosition(existing, ctx, buffer, timeStamp);
UpdateObjectFields(existing, ctx, buffer, timeStamp);
return existing;
}
private void UpdateObjectFields(InstancedObject obj, GameContext ctx, ProcessByteBuffer buffer, DateTime timeStamp)
{
int blockCount = (int)buffer.ReadByte();
if (blockCount > 0)
{
int masksize = blockCount << 2;
UpdateMask umask = buffer.ReadUpdateMask(masksize);
int limit = blockCount * 32;
for (int i = 0; i < limit; ++i)
{
if (umask.IsVisibleBit(i))
{
uint value = buffer.ReadUInt32();
switch (i)
{
case UpdateFields.UNIT_FIELD_TARGET + 1:
{
obj.setUInt32Value(i, value);
Logger.Log(Level.Trace, "{0} set target to {1}",
obj.getName(),
ctx.Data.SafeGetObjectName(obj.getValuesArray().GetGuid(UpdateFields.UNIT_FIELD_TARGET)));
}
break;
case UpdateFields.OBJECT_FIELD_TYPE:
{
WoWObjectType oldType = (WoWObjectType)obj.getValue(UpdateFields.OBJECT_FIELD_TYPE);
WoWObjectType newType = (WoWObjectType)value;
if ((oldType != newType) && (oldType != (WoWObjectType)0))
{
Logger.Log(Level.Trace, "Object {0} changing type from {1} to {2}...",
ctx.Data.SafeGetObjectName(obj.guid),
oldType,
newType); // does this ever happen?
}
}
break;
case UpdateFields.OBJECT_FIELD_ENTRY:
obj.SetTemplate((int)value, ctx.Data);
break;
case UpdateFields.UNIT_FIELD_CHARM + 1:
case UpdateFields.UNIT_FIELD_CHARMEDBY + 1:
case UpdateFields.UNIT_FIELD_CREATEDBY + 1:
case UpdateFields.UNIT_FIELD_SUMMON + 1:
case UpdateFields.UNIT_FIELD_SUMMONEDBY + 1:
{
if (obj is InstancedUnit) // useful to track changes in these fields; comment for general use
{
obj.setUInt32Value(i, value);
Logger.Log(Level.Trace, "{0} set {1} to {2}",
obj.getName(),
i - 1,
ctx.Data.SafeGetObjectName(obj.getGuid(i - 1)));
}
}
break;
case UpdateFields.UNIT_FIELD_FLAGS:
{
UnitFlags oldFlags = (UnitFlags)obj.getValue(UpdateFields.UNIT_FIELD_FLAGS);
UnitFlags newFlags = (UnitFlags)value;
}
break;
case UpdateFields.UNIT_FIELD_HEALTH:
{
uint oldValue = (uint)obj.getValue(UpdateFields.UNIT_FIELD_HEALTH);
uint newValue = (uint)value;
if ((oldValue != newValue) && (oldValue != 0))
{
if (newValue == 0)
{
Logger.Log(Level.Trace, "!!! {0} died !!!",
ctx.Data.SafeGetObjectName(obj.guid)); // is there a cleaner way to detect unit death?
}
else
{
Logger.Log(Level.Trace, "{0} changing health from {1} to {2}...",
ctx.Data.SafeGetObjectName(obj.guid),
oldValue,
newValue);
}
}
}
break;
}
obj.setUInt32Value(i, value);
}
}
}
}
[Flags]
public enum UpdateFlags : ushort
{
NONE = 0x00,
SELF = 0x01,
TRANSPORT = 0x02,
TargetGuid = 0x04,
LowGuid = 0x08,
HighGuid = 0x10,
Living = 0x20,
HasPosition = 0x40,
UNKNOWN1 = 0x80,
UNKNOWN2 = 0x100,
UNKNOWN3 = 0x200
}
private void UpdateObjectPosition(InstancedObject obj, GameContext ctx, ProcessByteBuffer buffer, DateTime movementTime)
{
UpdateFlags updateFlags = (UpdateFlags)buffer.ReadInt16();
MovementFlags movementFlags;
int timeStamp = 0;
short unknown1 = 0;
if ((updateFlags & UpdateFlags.Living) != 0)
{
{ // in 3.1.3, this block is held in 0x7a24d0
{ // in 3.1.3, this entire block is held in the function at 0x7A21C0
movementFlags = (MovementFlags)buffer.ReadInt32();
unknown1 = buffer.ReadInt16();
timeStamp = buffer.ReadInt32();
((InstancedWorldObject)obj).Position.Read(buffer, false);
((InstancedWorldObject)obj).Position.Timestamp = movementTime;
if ((movementFlags & MovementFlags.OnTransport) != 0)
{
ulong transportGuid = buffer.ReadPackedGuid(); // packed as of 3.1.3, see 0x7A222D
Position transportPos = new Position(0f, 0f, 0f, 0f);
transportPos.Read(buffer, false);
int transportTime = buffer.ReadInt32();
byte transportUnk = buffer.ReadByte();
((InstancedWorldObject)obj).IsOnTransport = true;
}
else
{
((InstancedWorldObject)obj).IsOnTransport = false;
}
if ((movementFlags & MovementFlags.Swimming) != 0)
{
((InstancedWorldObject)obj).IsSwimming = true;
}
else
{
((InstancedWorldObject)obj).IsSwimming = false;
}
// 3.1.3: 0x7a228a
if (((movementFlags & (MovementFlags.Swimming | MovementFlags.FLYING2)) != 0) ||
((unknown1 & 0x20) != 0))
{
float swimPitch = buffer.ReadFloat();
}
// 3.1.3: 0x7A22BA
int fallTime = buffer.ReadInt32();
if ((movementFlags & MovementFlags.Jumping) != 0)
{
((InstancedWorldObject)obj).IsJumping = true;
float jumpUnk1 = buffer.ReadFloat();
float jumpSinAngle = buffer.ReadFloat();
float jumpCosAngle = buffer.ReadFloat();
float jumpXYSpeed = buffer.ReadFloat();
}
else
{
((InstancedWorldObject)obj).IsJumping = false;
}
if ((movementFlags & MovementFlags.Spline) != 0)
{
float splineUnknown = buffer.ReadFloat();
}
}
for (int i = 0; i < 9; ++i)
{
buffer.ReadFloat(); // movementspeeds[i]
}
if ((movementFlags & MovementFlags.SPLINE2) != 0)
{
// 3.1.3: see sub 0x7A1FD0 (ProcessSplineFlags)
SplineFlags splineFlags = (SplineFlags)buffer.ReadInt32();
if ((splineFlags & SplineFlags.Target) != 0)
{
buffer.ReadFloat();
}
else
{
if ((splineFlags & SplineFlags.Point) != 0)
{
buffer.ReadWoWGuid();
}
else if ((splineFlags & SplineFlags.Unk1) != 0)
{
Position pos = new Position(0f, 0f, 0f, 0f);
pos.Read(buffer, true);
}
}
// 3.1.3: see 0x7a2028 thru end of func
buffer.ReadInt32();
buffer.ReadInt32();
buffer.ReadInt32();
buffer.ReadFloat();
buffer.ReadFloat();
buffer.ReadFloat();
buffer.ReadInt32();
int val = buffer.ReadInt32();
buffer.ReadBytes(12 * val);
buffer.ReadByte();
buffer.ReadFloat();
buffer.ReadFloat();
buffer.ReadFloat();
}
}
}
else // 0x7BC9EB
{
// this flag and check is new from 3.1.2->3.1.3 afaik
if ((updateFlags & UpdateFlags.UNKNOWN2) == 0)
{
if ((updateFlags & UpdateFlags.HasPosition) != 0)
{
((InstancedWorldObject)obj).Position.Read(buffer, false);
((InstancedWorldObject)obj).Position.Timestamp = movementTime;
}
}
else
{
// in 3.1.3, see the block at 0x7BC9F2
ulong guid2 = buffer.ReadPackedGuid();
for (int i = 0; i < 8; ++i)
{
buffer.ReadFloat(); // what is this??
}
}
}
if ((updateFlags & UpdateFlags.LowGuid) != 0)
{
uint lowGuid = buffer.ReadUInt32();
}
if ((updateFlags & UpdateFlags.HighGuid) != 0)
{
uint highGuid = buffer.ReadUInt32();
}
if ((updateFlags & UpdateFlags.TargetGuid) != 0)
{
ulong targetGuid = buffer.ReadPackedGuid(); // 3.1.3: 0x7BCADA
}
if ((updateFlags & UpdateFlags.TRANSPORT) != 0)
{
buffer.ReadInt32(); // 3.1.3: 0x7BCB01
}
if ((updateFlags & UpdateFlags.UNKNOWN1) != 0)
{
int wotlkUnk1 = buffer.ReadInt32();
float wotlkUnk2 = buffer.ReadFloat();
}
if ((updateFlags & UpdateFlags.UNKNOWN3) != 0)
{
buffer.ReadWoWGuid();
}
}
}
}
Edit: currently the problems I'm having with my bot are all higher-level abstractions. It's relatively easy to detect, for instance, who's attacking me (I can just enumerate threat lists). But a trickier task is how to normalize 2, 3, or 5 different "views" of the WoW gameworld so that my bot can get a clear idea of what's going on.