using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ComponentAce.Compression.Libs.zlib;
using OXGaming.TibiaAPI.Appearances;
using OXGaming.TibiaAPI.Constants;
using OXGaming.TibiaAPI.Creatures;
using OXGaming.TibiaAPI.DailyRewards;
using OXGaming.TibiaAPI.Imbuing;
using OXGaming.TibiaAPI.Market;
using OXGaming.TibiaAPI.Utilities;
namespace OXGaming.TibiaAPI.Network
{
///
/// The class contains methods for reading from, and writing to, a fix-sized byte array.
///
///
/// This is useful for parsing, and creating, Tibia packets.
///
public class NetworkMessage
{
private const uint PayloadDataPosition = 8;
private const int GroundLayer = 7;
private const int UndergroundLayer = 2;
private const int MapSizeX = 18;
private const int MapSizeY = 14;
private const int MapSizeZ = 8;
private const int MapSizeW = 10;
private const int MapMaxZ = 15;
///
/// The full length of a Tibia packet is stored in two bytes at the beginning of the packet.
/// This means that a Tibia packet can never be larger than 65535 + 2. Using a max size of 65535
/// ensures that limit is never exceeded.
///
public const ushort MaxMessageSize = ushort.MaxValue;
public const uint CompressedFlag = 0xC0000000;
private readonly byte[] _buffer = new byte[MaxMessageSize];
private Client _client;
private uint _size = PayloadDataPosition;
private bool _wasCompressed = false;
///
/// Gets the current position in the buffer.
///
public uint Position { get; private set; } = PayloadDataPosition;
public uint SequenceNumber
{
get
{
return BitConverter.ToUInt32(_buffer, 2);
}
set
{
var sequenceNumber = IsCompressed ? (value | CompressedFlag) : value;
var data = BitConverter.GetBytes(sequenceNumber);
Array.Copy(data, 0, _buffer, 2, data.Length);
}
}
///
/// Get/set the size of the message.
///
public uint Size { get => _size; set => _size = value; }
public bool IsCompressed
{
get
{
return (BitConverter.ToUInt32(_buffer, 2) & CompressedFlag) != 0;
}
set
{
if (value && !IsCompressed)
{
SequenceNumber |= CompressedFlag;
}
else if (!value && IsCompressed)
{
SequenceNumber ^= CompressedFlag;
}
}
}
public NetworkMessage(Client client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
SequenceNumber = ~CompressedFlag;
}
///
/// Gets the underlying buffer.
///
///
public byte[] GetBuffer()
{
return _buffer;
}
///
/// Gets the actual data from the underlying buffer.
///
///
/// Call this only when necessary as it creates a new byte array.
///
public byte[] GetData()
{
var data = new byte[Size];
Buffer.BlockCopy(_buffer, 0, data, 0, (int)Size);
return data;
}
public void Reset()
{
Position = 0;
Write(ushort.MinValue);
Write(~CompressedFlag);
Write(ushort.MinValue);
Size = PayloadDataPosition;
}
///
/// Reads the specified number of bytes from the buffer into an array of bytes
/// and advances the position by that number of bytes.
///
///
/// The number of bytes to read.
/// This value must be greater than 0 or an exception will occur.
///
///
/// An array of bytes containing the data read from the buffer.
///
///
/// Thrown when the 'count' parameter is less-than-or-equal-to 0.
///
///
/// Thrown when the position + the 'count' parameter exceeds the bounds of the buffer.
///
public byte[] ReadBytes(uint count)
{
if (count == 0)
{
throw new ArgumentOutOfRangeException(nameof(count),
"[NetworkMessage.ReadBytes] 'count' must be greater than 0.");
}
if (Position + count > _buffer.Length)
{
throw new IndexOutOfRangeException($"[NetworkMessage.ReadBytes] " +
$"'count' cannot exceed buffer size from current index. " +
$"index:{Position}, count:{count}, size:{Size}");
}
var data = new byte[count];
Array.Copy(_buffer, Position, data, 0, count);
Position += count;
return data;
}
///
/// Reads the next unsigned byte from the buffer and advances the position by one.
///
///
/// The next unsigned byte read from the buffer.
///
public byte ReadByte() => ReadBytes(1)[0];
///
/// Reads the next signed byte from the buffer and advances the position by one.
///
///
/// The next signed byte read from the buffer.
///
public sbyte ReadSByte() => unchecked((sbyte)ReadByte());
///
/// Reads the next byte from the buffer and advances the position by one.
///
///
/// A boolean value indicating whether the read byte was 0 or not.
/// 0 returns false, anything else returns true.
///
public bool ReadBool() => ReadByte() != 0;
///
/// Reads a 2-byte signed integer from the buffer and advances the position by two.
///
///
/// The 2-byte signed integer read from the buffer.
///
public short ReadInt16() => BitConverter.ToInt16(ReadBytes(2), 0);
///
/// Reads a 4-byte signed integer from the buffer and advances the position by four.
///
///
/// The 4-byte signed integer read from the buffer.
///
public int ReadInt32() => BitConverter.ToInt32(ReadBytes(4), 0);
///
/// Reads an 8-byte signed integer from the buffer and advances the position by eight.
///
///
/// The 8-byte signed integer read from the buffer.
///
public long ReadInt64() => BitConverter.ToInt64(ReadBytes(8), 0);
///
/// Reads a 2-byte unsigned integer from the buffer and advances the position by two.
///
///
/// The 2-byte unsigned integer read from the buffer.
///
public ushort ReadUInt16() => BitConverter.ToUInt16(ReadBytes(2), 0);
///
/// Reads a 4-byte unsigned integer from the buffer and advances the position by four.
///
///
/// The 4-byte unsigned integer read from the buffer.
///
public uint ReadUInt32() => BitConverter.ToUInt32(ReadBytes(4), 0);
///
/// Reads an 8-byte unsigned integer from the buffer and advances the position by eight.
///
///
/// The 8-byte unsigned integer read from the buffer.
///
public ulong ReadUInt64() => BitConverter.ToUInt64(ReadBytes(8), 0);
///
/// Reads the next byte and the following 4-byte unsigned integer from the buffer and advances the position by five.
///
///
/// A double value based on arithmetic done on the byte and 4-byte unsigned integer read from the buffer.
///
public double ReadDouble()
{
var num1 = ReadByte();
var num2 = ReadUInt32();
return (num2 - int.MaxValue) / Math.Pow(10, num1);
}
///
/// Reads a string from the buffer. The string is prefixed with the length in a 2-byte unsigned integer.
/// The position is advanced by 2 + the length of the string.
///
///
/// An ASCII encoded string based on the bytes read from the buffer.
///
public string ReadString()
{
var length = ReadUInt16();
return length == 0 ? string.Empty : Encoding.ASCII.GetString(ReadBytes(length));
}
public Position ReadPosition(int x = -1, int y = -1, int z = -1)
{
if (x == -1)
{
x = ReadUInt16();
}
if (y == -1)
{
y = ReadUInt16();
}
if (z == -1)
{
z = ReadByte();
}
return new Position(x, y, z);
}
public AppearanceInstance ReadMountOutfit()
{
var mountId = ReadUInt16();
return _client.AppearanceStorage.CreateOutfitInstance(mountId, 0, 0, 0, 0, 0);
}
public AppearanceInstance ReadCreatureOutfit()
{
var outfitId = ReadUInt16();
if (outfitId != 0)
{
var colorHead = ReadByte();
var colorTorso = ReadByte();
var colorLegs = ReadByte();
var colorDetail = ReadByte();
var addons = ReadByte();
return _client.AppearanceStorage.CreateOutfitInstance(outfitId, colorHead, colorTorso,
colorLegs, colorDetail, addons);
}
var itemId = ReadUInt16();
if (itemId == 0)
{
return _client.AppearanceStorage.CreateOutfitInstance(0, 0, 0, 0, 0, 0);
}
return _client.AppearanceStorage.CreateObjectInstance(itemId, 0);
}
public ObjectInstance ReadObjectInstance(ushort id = 0)
{
if (id == 0)
{
id = ReadUInt16();
}
if (id == 0)
{
return new ObjectInstance(id, null);
}
if (id <= 99)
{
throw new Exception($"[NetworkMessage.ReadObjectInstance] Invalid object id: {id}");
}
var objectInstance = _client.AppearanceStorage.CreateObjectInstance(id, 0);
if (objectInstance == null)
{
throw new Exception($"[NetworkMessage.ReadObjectInstance] Invalid object id: {id}");
}
var objectType = objectInstance.Type;
if (objectType == null)
{
return objectInstance;
}
if (_client.VersionNumber < 11887288)
{
objectInstance.Mark = ReadByte();
}
if (objectType.Flags.Liquidcontainer || objectType.Flags.Liquidpool || objectType.Flags.Cumulative)
{
objectInstance.Data = ReadByte();
}
if (_client.VersionNumber >= 11506055 && objectType.Flags.Container)
{
objectInstance.IsLootContainer = ReadBool();
if (objectInstance.IsLootContainer)
{
objectInstance.LootCategoryFlags = ReadUInt32();
}
}
if (objectType.FrameGroup[0].SpriteInfo.Animation != null)
{
objectInstance.Phase = ReadByte();
}
return objectInstance;
}
public Creature ReadCreatureInstance(int id = -1, Position position = null)
{
if (id == -1)
{
id = ReadUInt16();
}
if (id != (int)CreatureInstanceType.UnknownCreature &&
id != (int)CreatureInstanceType.OutdatedCreature &&
id != (int)CreatureInstanceType.Creature)
{
throw new Exception($"[NetworkMessage.ReadCreatureInstance] Invalid creature type: {id}");
}
Creature creature = null;
switch (id)
{
case (int)CreatureInstanceType.UnknownCreature:
{
var removeCreatureId = ReadUInt32();
var creatureId = ReadUInt32();
creature = (creatureId == _client.Player.Id) ? _client.Player : new Creature(creatureId);
creature.Type = (CreatureType)ReadByte();
creature.RemoveCreatureId = removeCreatureId;
creature.InstanceType = (CreatureInstanceType)id;
creature = _client.CreatureStorage.ReplaceCreature(creature, creature.RemoveCreatureId);
if (creature == null)
{
throw new Exception("[NetworkMessage.ReadCreatureInstance] Failed to append creature.");
}
if (creature.IsSummon)
{
creature.SummonerCreatureId = ReadUInt32();
}
creature.Name = ReadString();
creature.HealthPercent = ReadByte();
creature.Direction = (Direction)ReadByte();
creature.Outfit = ReadCreatureOutfit();
creature.Mount = ReadMountOutfit();
creature.Brightness = ReadByte();
creature.LightColor = ReadByte();
creature.Speed = ReadUInt16();
if (_client.VersionNumber >= 12409997)
{
// Always 0. I tried changing it to a different value for players, monsters,
// and NPCs (separately), but it would always crash the client.
creature.Unknown = ReadByte();
}
creature.PkFlag = ReadByte();
creature.PartyFlag = ReadByte();
creature.GuildFlag = ReadByte();
creature.Type = (CreatureType)ReadByte();
if (creature.Type == CreatureType.Player && _client.VersionNumber >= 12200000)
{
creature.Vocation = ReadByte();
}
else if (creature.IsSummon)
{
creature.SummonerCreatureId = ReadUInt32();
}
creature.SpeechCategory = ReadByte();
creature.Mark = ReadByte();
creature.InspectionState = ReadByte();
if (_client.VersionNumber < 11900000)
{
creature.PvpHelpers = ReadUInt16();
}
creature.IsUnpassable = ReadBool();
}
break;
case (int)CreatureInstanceType.OutdatedCreature:
{
var creatureId = ReadUInt32();
creature = _client.CreatureStorage.GetCreature(creatureId);
if (creature == null)
{
// This should never occur on official servers, but has been observed
// on Open-Tibia servers via cast system. Log the error and create
// a new Creature so that the parser can continue gracefully.
_client.Logger.Error("[NetworkMessage.ReadCreatureInstance] Outdated creature not found.");
creature = new Creature(creatureId);
creature = _client.CreatureStorage.ReplaceCreature(creature);
}
creature.InstanceType = (CreatureInstanceType)id;
creature.HealthPercent = ReadByte();
creature.Direction = (Direction)ReadByte();
creature.Outfit = ReadCreatureOutfit();
creature.Mount = ReadMountOutfit();
creature.Brightness = ReadByte();
creature.LightColor = ReadByte();
creature.Speed = ReadUInt16();
if (_client.VersionNumber >= 12409997)
{
// Always 0. I tried changing it to a different value for players, monsters,
// and NPCs (separately), but it would always crash the client.
creature.Unknown = ReadByte();
}
creature.PkFlag = ReadByte();
creature.PartyFlag = ReadByte();
creature.Type = (CreatureType)ReadByte();
if (creature.Type == CreatureType.Player && _client.VersionNumber >= 12200000)
{
creature.Vocation = ReadByte();
}
else if (creature.IsSummon)
{
creature.SummonerCreatureId = ReadUInt32();
}
creature.SpeechCategory = ReadByte();
creature.Mark = ReadByte();
creature.InspectionState = ReadByte();
if (_client.VersionNumber < 11900000)
{
creature.PvpHelpers = ReadUInt16();
}
creature.IsUnpassable = ReadBool();
}
break;
case (int)CreatureInstanceType.Creature:
{
var creatureId = ReadUInt32();
creature = _client.CreatureStorage.GetCreature(creatureId);
if (creature == null)
{
// This should never occur on official servers, but has been observed
// on Open-Tibia servers via cast system. Log the error and create
// a new Creature so that the parser can continue gracefully.
_client.Logger.Error("[NetworkMessage.ReadCreatureInstance] Known creature not found.");
creature = new Creature(creatureId);
creature = _client.CreatureStorage.ReplaceCreature(creature);
}
creature.InstanceType = (CreatureInstanceType)id;
creature.Direction = (Direction)ReadByte();
creature.IsUnpassable = ReadBool();
}
break;
}
if (position != null)
{
creature.Position = position;
}
return creature;
}
public Offer ReadMarketOffer(int kind, ushort typeId)
{
var timestamp = ReadUInt32();
var counter = ReadUInt16();
var itemId = typeId;
if (typeId == (int)MarketRequestType.OwnHistory ||
typeId == (int)MarketRequestType.OwnOffers)
{
itemId = ReadUInt16();
}
var amount = ReadUInt16();
var piecePrice = ReadUInt32();
var character = string.Empty;
var terminationReason = MarketOfferTerminationReason.Active;
if (typeId == (int)MarketRequestType.OwnHistory)
{
terminationReason = (MarketOfferTerminationReason)ReadByte();
}
else if (typeId == (int)MarketRequestType.OwnOffers)
{
}
else
{
character = ReadString();
}
return new Offer(new OfferId(timestamp, counter), kind, itemId, amount, piecePrice, character, terminationReason);
}
public ImbuementData ReadImbuementData()
{
var id = ReadUInt32();
var name = ReadString();
var imbuementData = new ImbuementData(id, name)
{
Description = ReadString(),
Category = ReadString(),
IconId = ReadUInt16(),
DurationInSeconds = ReadUInt32(),
PremiumOnly = ReadBool()
};
var astralSourceCount = ReadByte();
for (var i = 0; i < astralSourceCount; i++)
{
var astralId = ReadUInt16();
var astralName = ReadString();
var count = ReadUInt16();
imbuementData.AstralSources.Add(new AstralSource(astralId, count, astralName));
}
imbuementData.GoldCost = ReadUInt32();
imbuementData.SuccessRatePercent = ReadByte();
imbuementData.ProtectionGoldCost = ReadUInt32();
return imbuementData;
}
public DailyReward ReadDailyReward()
{
var dailyReward = new DailyReward
{
Type = ReadByte()
};
switch (dailyReward.Type)
{
case 1: // Choice
{
dailyReward.TotalChoiceRewards = ReadByte();
dailyReward.ChoiceRewards.Capacity = ReadByte();
for (var i = 0; i < dailyReward.ChoiceRewards.Capacity; ++i)
{
var itemId = ReadUInt16();
var name = ReadString();
var weight = ReadUInt32();
dailyReward.ChoiceRewards.Add((itemId, name, weight));
}
}
break;
case 2: // Set
{
dailyReward.SetRewards.Capacity = ReadByte();
for (var i = 0; i < dailyReward.SetRewards.Capacity; ++i)
{
var type = ReadByte();
switch (type)
{
case 1: // Item
{
var itemId = ReadUInt16();
var name = ReadString();
var count = ReadByte();
dailyReward.SetRewards.Add((type, itemId, name, count, 0));
}
break;
case 2: // Prey Bonus Reroll
{
var count = ReadByte();
dailyReward.SetRewards.Add((type, 0, "", count, 0));
}
break;
case 3: // XP Boost
{
var duration = ReadUInt16();
dailyReward.SetRewards.Add((type, 0, "", 0, duration));
}
break;
default:
_client.Logger.Error($"Unknown SetDailyReward type: {type}");
break;
}
}
}
break;
default:
_client.Logger.Debug($"Unknown DailyReward type: {dailyReward.Type}");
break;
}
return dailyReward;
}
public int ReadField(int x, int y, int z, List<(int, List, Position)> fields)
{
var hasSetEnvironmentalEffect = false;
var thingsCount = 0;
var numberOfTilesToSkip = 0;
var mapPosition = new Position(x, y, z);
var absolutePosition = _client.WorldMapStorage.ToAbsolute(mapPosition);
var objects = new List();
while (true)
{
var thingId = ReadUInt16();
if (thingId >= 0xFF00)
{
numberOfTilesToSkip = thingId - 0xFF00;
break;
}
if (!hasSetEnvironmentalEffect)
{
hasSetEnvironmentalEffect = true;
if (_client.VersionNumber < 11880000)
{
continue;
}
}
if (thingId == (int)CreatureInstanceType.UnknownCreature ||
thingId == (int)CreatureInstanceType.OutdatedCreature ||
thingId == (int)CreatureInstanceType.Creature)
{
var creature = ReadCreatureInstance(thingId, absolutePosition);
var objectInstance = _client.AppearanceStorage.CreateObjectInstance((uint)CreatureInstanceType.Creature, creature.Id);
if (thingsCount < MapSizeW)
{
objects.Add(objectInstance);
_client.WorldMapStorage.AppendObject(x, y, z, objectInstance);
}
}
else
{
var objectInstance = ReadObjectInstance(thingId);
if (thingsCount < MapSizeW)
{
objects.Add(objectInstance);
_client.WorldMapStorage.AppendObject(x, y, z, objectInstance);
}
else
{
throw new Exception("Connection.readField: Expected creatures but received regular object.");
}
}
thingsCount++;
}
fields.Add((numberOfTilesToSkip, objects, absolutePosition));
return numberOfTilesToSkip;
}
public int ReadFloor(int floorNumber, int numberOfTilesToSkip, List<(int, List, Position)> fields)
{
if (floorNumber < 0 || floorNumber >= MapSizeZ)
{
throw new Exception("ReadFloor: Floor number out of range.");
}
var currentX = 0;
var currentY = 0;
while (currentX <= MapSizeX - 1)
{
currentY = 0;
while (currentY <= MapSizeY - 1)
{
if (numberOfTilesToSkip > 0)
{
numberOfTilesToSkip--;
}
else
{
numberOfTilesToSkip = ReadField(currentX, currentY, floorNumber, fields);
}
currentY++;
}
currentX++;
}
return numberOfTilesToSkip;
}
public int ReadArea(int startX, int startY, int endX, int endY, List<(int, List, Position)> fields)
{
var endZ = 0;
var stepZ = 0;
var numberOfTilesToSkip = 0;
var currentX = 0;
var currentY = 0;
var currentZ = 0;
var position = _client.WorldMapStorage.GetPosition();
if (position.Z <= GroundLayer)
{
currentZ = 0;
endZ = GroundLayer + 1;
stepZ = 1;
}
else
{
currentZ = 2 * UndergroundLayer;
endZ = Math.Max(-1, position.Z - MapMaxZ + 1);
stepZ = -1;
}
while (currentZ != endZ)
{
currentX = startX;
while (currentX <= endX)
{
currentY = startY;
while (currentY <= endY)
{
if (numberOfTilesToSkip > 0)
{
numberOfTilesToSkip--;
}
else
{
numberOfTilesToSkip = ReadField(currentX, currentY, currentZ, fields);
}
currentY++;
}
currentX++;
}
currentZ += stepZ;
}
return numberOfTilesToSkip;
}
///
/// Writes a byte array to the buffer.
///
///
/// A byte array containing the data to write.
///
///
/// Thrown when is null.
///
///
/// Thrown when the position + the length of 'value' exceeds the bounds of the buffer.
///
public void Write(byte[] value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (Position + value.Length > _buffer.Length)
{
throw new IndexOutOfRangeException($"[NetworkMessage.Write] " +
$"'value' length cannot exceed buffer size from current index. " +
$"index:{Position}, value length:{value.Length}, size:{Size}");
}
Array.Copy(value, 0, _buffer, Position, value.Length);
Position += (uint)value.Length;
if (Position > Size)
{
Size = Position;
}
}
public void Write(byte[] value, uint index, uint length)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (length > value.Length || index + length > value.Length)
{
throw new IndexOutOfRangeException($"[NetworkMessage.Write] " +
$"length cannot exceed value length from index. " +
$"index:{index}, value length:{value.Length}, length:{length}");
}
var data = new byte[length];
Array.Copy(value, index, data, 0, length);
Write(data);
}
///
/// Writes an unsigned byte to the buffer and advances the position by one.
///
///
/// The unsigned byte to write.
///
public void Write(byte value) => Write(new byte[] { value });
///
/// Writes a signed byte to the buffer and advances the position by one.
///
///
/// The signed byte to write.
///
public void Write(sbyte value) => Write(new byte[] { unchecked((byte)value) });
///
/// Writes a boolean value, as an unsigned byte, to the buffer and advances the position by one.
///
///
/// The boolean value to write.
///
public void Write(bool value) => Write((byte)(value ? 1 : 0));
///
/// Writes a two-byte signed integer to the buffer and advances the position by two.
///
///
/// The two-byte signed integer to write.
///
public void Write(short value) => Write(BitConverter.GetBytes(value));
///
/// Writes a four-byte signed integer to the buffer and advances the position by four.
///
///
/// The four-byte signed integer to write.
///
public void Write(int value) => Write(BitConverter.GetBytes(value));
///
/// Writes an eight-byte signed integer to the buffer and advances the position by eight.
///
///
/// The eight-byte signed integer to write.
///
public void Write(long value) => Write(BitConverter.GetBytes(value));
///
/// Writes a two-byte signed integer to the buffer and advances the position by two.
///
///
/// The two-byte signed integer to write.
///
public void Write(ushort value) => Write(BitConverter.GetBytes(value));
///
/// Writes a four-byte signed integer to the buffer and advances the position by four.
///
///
/// The four-byte signed integer to write.
///
public void Write(uint value) => Write(BitConverter.GetBytes(value));
///
/// Writes an eight-byte signed integer to the buffer and advances the position by eight.
///
///
/// The eight-byte signed integer to write.
///
public void Write(ulong value) => Write(BitConverter.GetBytes(value));
///
/// Writes an unsigned byte (precision), followed by a 4-byte unsigned integer based on arithmetic done on 'value',
/// to the buffer and advances the position by five.
///
///
/// The eight-byte floating point value to write.
///
public void Write(double value)
{
const byte precision = 3;
Write(precision);
Write((uint)((value * Math.Pow(10, precision)) + int.MaxValue));
}
///
/// Writes a length-prefixed string to the buffer with ASCII encoding.
/// The position is advanced by 2 + the length of 'value'.
///
///
public void Write(string value)
{
if (value == null)
{
value = string.Empty;
}
Write((ushort)value.Length);
if (value.Length > 0)
{
Write(Encoding.ASCII.GetBytes(value));
}
}
public void Write(Position value)
{
Write((ushort)value.X);
Write((ushort)value.Y);
Write((byte)value.Z);
}
public void Write(OutfitInstance value)
{
Write((ushort)value.Id);
if (value.Id == 0)
{
Write((ushort)0);
}
else
{
Write(value.ColorHead);
Write(value.ColorTorso);
Write(value.ColorLegs);
Write(value.ColorDetail);
Write(value.Addons);
}
}
public void Write(ObjectInstance value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
Write((ushort)value.Id);
if (value.Type == null)
{
return;
}
if (_client.VersionNumber < 11900000)
{
Write(value.Mark);
}
if (value.Type.Flags.Liquidcontainer || value.Type.Flags.Liquidpool || value.Type.Flags.Cumulative)
{
Write((byte)value.Data);
}
if (value.Type.Flags.Container)
{
Write(value.IsLootContainer);
if (value.IsLootContainer)
{
Write(value.LootCategoryFlags);
}
}
if (value.Type.FrameGroup[0].SpriteInfo.Animation != null)
{
Write(value.Phase);
}
}
public void Write(Creature value, CreatureInstanceType type)
{
switch (type)
{
case CreatureInstanceType.UnknownCreature:
{
Write(value.RemoveCreatureId);
Write(value.Id);
Write((byte)value.Type);
if (value.IsSummon)
{
Write(value.SummonerCreatureId);
}
Write(value.Name);
Write(value.HealthPercent);
Write((byte)value.Direction);
if (value.Outfit is OutfitInstance)
{
Write((OutfitInstance)value.Outfit);
}
else
{
Write((ushort)0);
Write((ushort)value.Outfit.Id);
}
Write((ushort)value.Mount.Id);
Write(value.Brightness);
Write(value.LightColor);
Write(value.Speed);
if (_client.VersionNumber >= 12409997)
{
Write(value.Unknown);
}
Write(value.PkFlag);
Write(value.PartyFlag);
Write(value.GuildFlag);
Write((byte)value.Type);
if (value.Type == CreatureType.Player && _client.VersionNumber >= 12200000)
{
Write(value.Vocation);
}
else if (value.IsSummon)
{
Write(value.SummonerCreatureId);
}
Write(value.SpeechCategory);
Write(value.Mark);
Write(value.InspectionState);
if (_client.VersionNumber < 11900000)
{
Write(value.PvpHelpers);
}
Write(value.IsUnpassable);
}
break;
case CreatureInstanceType.OutdatedCreature:
{
Write(value.Id);
Write(value.HealthPercent);
Write((byte)value.Direction);
if (value.Outfit is OutfitInstance instance)
{
Write(instance);
}
else
{
Write((ushort)0);
Write((ushort)value.Outfit.Id);
}
Write((ushort)value.Mount.Id);
Write(value.Brightness);
Write(value.LightColor);
Write(value.Speed);
if (_client.VersionNumber >= 12409997)
{
Write(value.Unknown);
}
Write(value.PkFlag);
Write(value.PartyFlag);
Write((byte)value.Type);
if (value.Type == CreatureType.Player && _client.VersionNumber >= 12200000)
{
Write(value.Vocation);
}
else if (value.IsSummon)
{
Write(value.SummonerCreatureId);
}
Write(value.SpeechCategory);
Write(value.Mark);
Write(value.InspectionState);
if (_client.VersionNumber < 11900000)
{
Write(value.PvpHelpers);
}
Write(value.IsUnpassable);
}
break;
case CreatureInstanceType.Creature:
{
Write(value.Id);
Write((byte)value.Direction);
Write(value.IsUnpassable);
}
break;
}
}
public void Write(Offer value, ushort typeId)
{
Write(value.OfferId.Timestamp);
Write(value.OfferId.Counter);
if (typeId == (int)MarketRequestType.OwnHistory ||
typeId == (int)MarketRequestType.OwnOffers)
{
Write(value.TypeId);
}
Write(value.Amount);
Write(value.PiecePrice);
if (typeId == (int)MarketRequestType.OwnHistory)
{
Write((byte)value.TerminationReason);
}
else if (typeId == (int)MarketRequestType.OwnOffers)
{
}
else
{
Write(value.Character);
}
}
public void Write(ImbuementData value)
{
Write(value.Id);
Write(value.Name);
Write(value.Description);
Write(value.Category);
Write(value.IconId);
Write(value.DurationInSeconds);
Write(value.PremiumOnly);
var count = (byte)Math.Min(value.AstralSources.Count, byte.MaxValue);
Write(count);
for (var i = 0; i < count; ++i)
{
var astralSource = value.AstralSources[i];
Write(astralSource.AppearanceTypeId);
Write(astralSource.Name);
Write(astralSource.ObjectCount);
}
Write(value.GoldCost);
Write(value.SuccessRatePercent);
Write(value.ProtectionGoldCost);
}
public void Write(DailyReward dailyReward)
{
Write(dailyReward.Type);
switch (dailyReward.Type)
{
case 1: // Choice
{
Write(dailyReward.TotalChoiceRewards);
var count = Math.Min(dailyReward.ChoiceRewards.Count, byte.MaxValue);
Write((byte)count);
for (var i = 0; i < count; ++i)
{
Write(dailyReward.ChoiceRewards[i].ItemId);
Write(dailyReward.ChoiceRewards[i].Name);
Write(dailyReward.ChoiceRewards[i].Weight);
}
}
break;
case 2: // Set
{
var count = Math.Min(dailyReward.SetRewards.Count, byte.MaxValue);
Write((byte)count);
for (var i = 0; i < count; ++i)
{
Write(dailyReward.SetRewards[i].Type);
switch (dailyReward.SetRewards[i].Type)
{
case 1: // Item
{
Write(dailyReward.SetRewards[i].ItemId);
Write(dailyReward.SetRewards[i].Name);
Write(dailyReward.SetRewards[i].Count);
}
break;
case 2: // Prey Bonus Rerolls
{
Write(dailyReward.SetRewards[i].Count);
}
break;
case 3: // XP Boost
{
Write(dailyReward.SetRewards[i].Duration);
}
break;
}
}
}
break;
}
}
///
/// Reads the specified number of bytes from the buffer into an array of bytes.
///
///
/// The number of bytes to read.
/// This value must be greater than 0 or an exception will occur.
///
///
/// An array of bytes containing the data read from the buffer.
///
///
/// Thrown when the 'count' parameter is less-than-or-equal-to 0.
///
///
/// Thrown when the position + the 'count' parameter exceeds the bounds of the buffer.
///
public byte[] PeekBytes(uint count)
{
if (count == 0)
{
throw new ArgumentOutOfRangeException(nameof(count),
"[NetworkMessage.PeekBytes] 'count' must be greater than 0.");
}
if (Position + count > _buffer.Length)
{
throw new IndexOutOfRangeException($"[NetworkMessage.PeekBytes] " +
$"'count' cannot exceed buffer size from current index. " +
$"index:{Position}, count:{count}, size:{Size}");
}
var data = new byte[count];
Array.Copy(_buffer, Position, data, 0, count);
return data;
}
///
/// Reads the next byte from the buffer.
///
///
/// The next byte Peek from the buffer.
///
public byte PeekByte() => PeekBytes(1)[0];
///
/// Reads the next byte from the buffer.
///
///
/// A boolean value indicating whether the read byte was 0 or not.
/// 0 returns false, anything else returns true.
///
public bool PeekBool() => PeekByte() != 0;
///
/// Reads a 2-byte signed integer from the buffer.
///
///
/// The 2-byte signed integer read from the buffer.
///
public short PeekInt16() => BitConverter.ToInt16(PeekBytes(2), 0);
///
/// Reads a 4-byte signed integer from the buffer.
///
///
/// The 4-byte signed integer read from the buffer.
///
public int PeekInt32() => BitConverter.ToInt32(PeekBytes(4), 0);
///
/// Reads an 8-byte signed integer from the buffer.
///
///
/// The 8-byte signed integer read from the buffer.
///
public long PeekInt64() => BitConverter.ToInt64(PeekBytes(8), 0);
///
/// Reads a 2-byte unsigned integer from the buffer.
///
///
/// The 2-byte unsigned integer read from the buffer.
///
public ushort PeekUInt16() => BitConverter.ToUInt16(PeekBytes(2), 0);
///
/// Reads a 4-byte unsigned integer from the buffer.
///
///
/// The 4-byte unsigned integer read from the buffer.
///
public uint PeekUInt32() => BitConverter.ToUInt32(PeekBytes(4), 0);
///
/// Reads an 8-byte unsigned integer from the buffer.
///
///
/// The 8-byte unsigned integer read from the buffer.
///
public ulong PeekUInt64() => BitConverter.ToUInt64(PeekBytes(8), 0);
///
/// Reads the next byte and the following 4-byte unsigned integer from the buffer.
///
///
/// A double value based on arithmetic done on the byte and 4-byte unsigned integer read from the buffer.
///
public double PeekDouble()
{
var num1 = PeekByte();
var num2 = PeekUInt32();
return (num2 - int.MaxValue) / Math.Pow(10, num1);
}
///
/// Reads a string from the buffer. The string is prefixed with the length in a 2-byte unsigned integer.
///
///
/// An ASCII encoded string based on the bytes read from the buffer.
///
public string PeekString()
{
var length = PeekUInt16();
return Encoding.ASCII.GetString(PeekBytes(length));
}
///
/// Sets the position of the buffer.
///
///
/// The offset, from the , to set the position.
///
///
/// The position in the buffer to seek from. Can be the beginning of the buffer, the current position, or the end of the buffer.
///
public void Seek(int offset, SeekOrigin origin)
{
if (origin == SeekOrigin.Begin)
{
if (offset < 0 || offset >= MaxMessageSize)
{
throw new ArgumentOutOfRangeException($"");
}
Position = (uint)offset;
}
else if (origin == SeekOrigin.Current)
{
if ((Position + offset < 0) || (Position + offset >= MaxMessageSize))
{
throw new ArgumentOutOfRangeException($"");
}
if (offset >= 0)
{
Position += (uint)offset;
}
else
{
Position -= (uint)offset;
}
}
else if (origin == SeekOrigin.End)
{
if (offset > 0 || offset <= MaxMessageSize)
{
throw new ArgumentOutOfRangeException($"");
}
Position = MaxMessageSize - (uint)offset - 1;
}
}
public void PrepareToParse(uint[] xteaKey, ZStream zStream = null)
{
if (xteaKey != null)
{
Xtea.Decrypt(_buffer, Size, xteaKey);
}
if (IsCompressed && zStream != null)
{
var compressedSize = BitConverter.ToUInt16(_buffer, 6);
var inBuffer = new byte[compressedSize];
var outBuffer = new byte[MaxMessageSize];
Array.Copy(_buffer, 8, inBuffer, 0, compressedSize);
zStream.next_in = inBuffer;
zStream.next_in_index = 0;
zStream.avail_in = inBuffer.Length;
zStream.next_out = outBuffer;
zStream.next_out_index = 0;
zStream.avail_out = outBuffer.Length;
var ret = zStream.inflate(zlibConst.Z_SYNC_FLUSH);
if (ret != zlibConst.Z_OK)
{
throw new Exception($"[NetworkMessage.PrepareToParse] zlib inflate failed: {ret}");
}
Position = 2;
_wasCompressed = true;
Write(SequenceNumber ^ CompressedFlag);
Write((ushort)zStream.next_out_index);
Write(outBuffer, 0, (uint)zStream.next_out_index);
}
Position = 6;
Size = ReadUInt16() + PayloadDataPosition;
}
public void PrepareToSend(uint[] xteaKey, ZStream zStream = null)
{
if (_wasCompressed && zStream != null)
{
var uncompressedSize = _size - 8;
var inBuffer = new byte[uncompressedSize];
var outBuffer = new byte[MaxMessageSize];
Array.Copy(_buffer, 8, inBuffer, 0, uncompressedSize);
zStream.next_in = inBuffer;
zStream.next_in_index = 0;
zStream.avail_in = inBuffer.Length;
zStream.next_out = outBuffer;
zStream.next_out_index = 0;
zStream.avail_out = outBuffer.Length;
var ret = zStream.deflate(zlibConst.Z_SYNC_FLUSH);
if (ret != zlibConst.Z_OK)
{
throw new Exception($"[NetworkMessage.PrepareToSend] zlib deflate failed: {ret}");
}
// deflate appends a footer (0x00 0x00 0xFF 0xFF) to the end of the data,
// but we don't need it so ignore it when copying the data.
var data = new byte[zStream.next_out_index - 4];
Array.Copy(outBuffer, data, data.Length);
Position = 8;
Size = 8;
Write(outBuffer, 0, (uint)(zStream.next_out_index - 4));
if (!IsCompressed)
{
Position = 2;
Write(SequenceNumber + CompressedFlag);
}
}
Position = 6;
Write((ushort)(Size - PayloadDataPosition));
if (xteaKey != null)
{
Xtea.Encrypt(_buffer, ref _size, xteaKey);
}
Position = 0;
Write((ushort)(_size - 2));
}
}
}