Getting started
This guide covers the main parts of the eolib-dotnet API.
Namespaces
| Namespace | Contents |
|---|---|
Moffat.EndlessOnline.SDK.Data |
EoReader, EoWriter, number/string encoding, packet encryption and server verification |
Moffat.EndlessOnline.SDK.Packet |
Packet sequencing |
Moffat.EndlessOnline.SDK.Protocol |
ISerializable, PacketResolver, and the types shared by the rest of the protocol |
Moffat.EndlessOnline.SDK.Protocol.Net |
IPacket, PacketFamily, PacketAction, and the types shared by client and server packets |
Moffat.EndlessOnline.SDK.Protocol.Net.Client, Moffat.EndlessOnline.SDK.Protocol.Net.Server |
Client or server packets |
Moffat.EndlessOnline.SDK.Protocol.Pub, Moffat.EndlessOnline.SDK.Protocol.Pub.Server |
Client and server pub files |
Moffat.EndlessOnline.SDK.Protocol.Map |
Map files |
Naming conventions
- Namespaces follow the protocol XML layout.
- Types, properties and enum values are PascalCase versions of the protocol XML names. Packets are named
<Family><Action><Client|Server>Packet, for exampleWalkPlayerClientPacket.
Protocol types
Generated types implement ISerializable, which provides Serialize(EoWriter), Deserialize(EoReader) and
ByteSize. They also override ToString(), Equals() and GetHashCode().
| Protocol type | .NET type |
|---|---|
byte, char, short, three, int |
int |
bool |
bool |
string, encoded_string |
string (Windows-1252) |
blob |
byte[] |
| enum | enum. Unrecognized values are preserved. |
| array | List<T> |
| optional field | Nullable type (int?, string?, ...) |
| switch | An interface named I<Field>Data, implemented by a nested class for each case, and For... factory methods on the containing type |
Packets
Every packet implements IPacket, which adds the Family and Action properties.
using Moffat.EndlessOnline.SDK.Data;
using Moffat.EndlessOnline.SDK.Protocol;
using Moffat.EndlessOnline.SDK.Protocol.Net;
using Moffat.EndlessOnline.SDK.Protocol.Net.Client;
var walk = new WalkPlayerClientPacket
{
WalkAction = new WalkAction
{
Direction = Direction.Up,
Coords = new Coords { X = 5, Y = 7 },
},
};
var writer = new EoWriter();
walk.Serialize(writer);
byte[] payload = writer.ToByteArray();
PacketResolver creates packets from their family and action. It throws InvalidOperationException for unknown
packets:
var resolver = new PacketResolver("Moffat.EndlessOnline.SDK.Protocol.Net.Client");
IPacket packet = resolver.Create(PacketFamily.Walk, PacketAction.Player);
packet.Deserialize(new EoReader(payload));
if (packet is WalkPlayerClientPacket received)
{
// use received.WalkAction
}
Switch fields are properties named <Field>Data, which hold the case class that matches the switch field. Types with a switch have static factory methods that set the switch field and its data together:
using Moffat.EndlessOnline.SDK.Protocol.Net.Server;
var init = InitInitServerPacket.ForOk(new InitInitServerPacket.ReplyCodeDataOk { PlayerId = 1 });
// Cases without data to set, and enum values without a case, take no parameters
var wrongUser = LoginReplyServerPacket.ForWrongUser();
// Nested switches are flattened: this sets ReplyCode to Banned and BanType to Permanent
var banned = InitInitServerPacket.ForBannedPermanent();
// Default cases take the value of the switch field, which must not have its own case
int sessionId = 1234;
var created = AccountReplyServerPacket.ForReplyCodeDefault(
(AccountReply)sessionId,
new AccountReplyServerPacket.ReplyCodeDataDefault { SequenceStart = 5 });
| Case | Factory |
|---|---|
| Enum value whose case has data | For<Value>(data), e.g. LoginReplyServerPacket.ForOk(data) |
| Enum value with an empty case, a case with only hardcoded data, or no case | For<Value>(), e.g. LoginReplyServerPacket.ForWrongUser() |
| Enum value whose case has a nested switch | For<Value><InnerValue>, e.g. InitInitServerPacket.ForBannedTemporary(data) |
| Numeric case with data | For<CaseClass>(data), e.g. InitInitServerPacket.ForBanTypeData0(data) |
| Default case | For<Field>Default(code, data), e.g. AccountReplyServerPacket.ForReplyCodeDefault(code, data) |
Numeric cases without data don't have a factory. Factories throw ArgumentNullException for null data, and default case factories throw ArgumentException for a value that has its own case. The properties can still be set directly, as long as the case class matches the switch field.
To read a switch, check the type of the data with pattern matching:
if (packet.ReplyCodeData is LoginReplyServerPacket.ReplyCodeDataOk ok)
{
// use ok.Characters
}
There's no need to check the switch field as well: deserializing always sets the case class that matches the switch field, and serializing throws InvalidOperationException when the data isn't the case class for the switch field.
Pub and map files
using Moffat.EndlessOnline.SDK.Protocol.Pub;
var eif = new Eif();
eif.Deserialize(new EoReader(File.ReadAllBytes("dat001.eif")));
foreach (var item in eif.Items)
{
Console.WriteLine(item.Name);
}
Enf, Esf and Ecf, the Protocol.Pub.Server files and Protocol.Map.Emf work the same way.
Encryption and sequencing
DataEncrypter transforms the packet bytes after the 2-byte length prefix, and returns the result:
// Encrypt
bytes = DataEncrypter.SwapMultiples(bytes, 6);
bytes = DataEncrypter.Interleave(bytes);
bytes = DataEncrypter.FlipMSB(bytes);
// Decrypt
bytes = DataEncrypter.FlipMSB(bytes);
bytes = DataEncrypter.Deinterleave(bytes);
bytes = DataEncrypter.SwapMultiples(bytes, 6);
A server generates a sequence start for the init handshake and tracks the sequence for each connection:
using Moffat.EndlessOnline.SDK.Packet;
var start = InitSequenceStart.Generate(new Random());
var sequencer = new PacketSequencer(start);
// Send start.Seq1 and start.Seq2 in the init reply, then check each client packet's sequence number
int expected = sequencer.NextSequence();
PacketSequencer.WithSequenceStart returns a sequencer for a new start, such as the PingSequenceStart a server
sends with connection pings.
Reading and writing data
EoReader and EoWriter read and write EO numbers and strings directly, for data that isn't generated from the
protocol:
var writer = new EoWriter();
writer.AddShort(1234);
writer.AddString("hello");
var reader = new EoReader(writer.ToByteArray());
int number = reader.GetShort();
string text = reader.GetString();
Reading past the end of the data returns 0 or an empty value instead of throwing, which matches the official
client. EoReader.ChunkedReadingMode and NextChunk() read data split into chunks by 0xFF break bytes.