eolib-cpp 0.1.0-beta.7
Core C++ library for writing Endless Online applications
Loading...
Searching...
No Matches
Getting started

This guide covers the main parts of the eolib API. Include everything with <eolib/eolib.hpp>, or include only the headers you need:

Header Contents
eolib/data/eo_reader.hpp, eolib/data/eo_writer.hpp EoReader, EoWriter
eolib/data/number_encoder.hpp, eolib/data/string_encoder.hpp EO number/string encoding
eolib/encrypt/data_encrypter.hpp, eolib/encrypt/server_verifier.hpp Packet encryption, server verification
eolib/packet/packet_sequencer.hpp, eolib/packet/sequence_start.hpp Packet sequencing
eolib/protocol.hpp All generated protocol code
eolib/protocol/net/client.hpp, eolib/protocol/net/server.hpp Client or server packets only
eolib/protocol/pub.hpp, eolib/protocol/map.hpp Pub and map files

Naming conventions

Protocol types

Generated types derive from eolib::protocol::Serializable, which provides Serialize(EoWriter&), Deserialize(EoReader&), ByteSize() and ToString(), and can be written to a std::ostream with operator<< (unless the library is built with EOLIB_STRUCT_STRINGS=OFF). Enums have ToString(value) and operator<< too, unless the library is built with EOLIB_ENUM_STRINGS=OFF. Structs also have operator== and operator!=.

Protocol type C++ type
byte, char, short, three, int int
bool bool
string, encoded_string std::string (raw bytes, no transcoding)
blob std::vector<std::uint8_t>
enum enum class : int. Unrecognized values are preserved.
array std::vector<T>
optional field std::optional<T>
switch std::variant<std::monostate, ...> of generated case structs

Numeric types

All EO numbers are int, as in eolib-dotnet and eolib-java. Sizes, positions and lengths are int too, since protocol lengths can have negative offsets applied. Only raw byte data (blob, GetBytes, pointer + length overloads) uses std::uint8_t and std::size_t.

  • EO int values can be up to 4,097,152,080, which does not fit in a 32-bit int. Values above INT_MAX are read as negative numbers. AddInt accepts them back, so they round-trip, but they can't be written as positive values.
  • Malformed data can decode to negative or out-of-range values; for example, a 0x00 byte decodes as -1.
  • Validate numbers read from untrusted data (such as client packets on a server) before using them, for example by rejecting negative amounts.

Packets

Every packet derives from eolib::protocol::net::Packet, which adds Family() and Action(). Packet classes also have FAMILY and ACTION constants.

using namespace eolib;
using namespace eolib::protocol::net;
walk.walk_action.coords.x = 5;
walk.walk_action.coords.y = 7;
walk.Serialize(writer);
std::vector<std::uint8_t> payload = writer.ToByteArray();
A class for writing EO data to a sequence of bytes.
Definition eo_writer.hpp:18
std::vector< std::uint8_t > ToByteArray() const &
Gets a copy of the writer data as a byte array.
int x
Definition structs.hpp:27
int y
Definition structs.hpp:31
protocol::Direction direction
Definition structs.hpp:78
protocol::Coords coords
Definition structs.hpp:84
Walking.
Definition packets.hpp:3043
void Serialize(data::EoWriter &writer) const override
Serializes this object to the provided writer.
WalkAction walk_action
Definition packets.hpp:3051
Types shared by client and server packets.
@ Up
Definition enums.hpp:47
Core library for writing Endless Online applications.

PacketFactory creates packets from their family and action. It returns nullptr for unknown packets:

data::EoReader reader(payload);
if (auto* received = dynamic_cast<client::WalkPlayerClientPacket*>(packet.get()))
{
// use received->walk_action
}
A class for reading EO data from a sequence of bytes.
Definition eo_reader.hpp:32
static std::unique_ptr< Packet > Deserialize(PacketFamily family, PacketAction action, data::EoReader &reader)
Creates a client packet with the specified family and action, and deserializes it from the reader.
Packet sequencing.
@ Walk
Definition enums.hpp:21
@ Player
Definition enums.hpp:96

To check whether a family and action identify a known packet without creating it, use PacketFactory::Contains:

if (!client::PacketFactory::Contains(family, action))
{
// unknown packet
}
static bool Contains(PacketFamily family, PacketAction action) noexcept
Checks whether a client packet exists with the specified family and action, without creating it.

EoReader's constructors copy (or take ownership of) the data. To read a buffer you own without copying it, use EoReader::View. The buffer must outlive the reader and its slices; generated types copy their fields out, so it only needs to live until Deserialize returns:

void OnPacket(const std::string& payload)
{
auto packet = client::PacketFactory::Deserialize(family, action, reader);
}
static EoReader View(std::string_view data) noexcept
Creates a reader that refers to the specified data without copying it.

Switch fields are std::variant members named <field>_data. Set the alternative that matches the switch field, or serialization throws eolib::SerializationError. The generated For<Case> factories set the switch field and its data together:

ok.player_id = 1;
Data associated with reply_code value Ok.
Definition packets.hpp:87
static InitInitServerPacket ForBannedPermanent()
Creates a new InitInitServerPacket with reply_code set to Banned and ban_type set to Permanent.
static InitInitServerPacket ForOk(ReplyCodeDataOk data)
Creates a new InitInitServerPacket with reply_code set to Ok.

There is a factory for each value of the switch field's enum:

Case Factory
A value whose case has data members For<Value>(data), e.g. LoginReplyServerPacket::ForOk(data)
A value whose case has no data members, or that has no case For<Value>(), e.g. LoginReplyServerPacket::ForWrongUser()
A value whose case contains another switch One factory per value of the inner switch, e.g. InitInitServerPacket::ForBannedTemporary(data)
An integer case with data For<CaseClass>(data), e.g. InitInitServerPacket::ForBanTypeData0(data)
The default case For<Field>Default(code[, data]), e.g. AccountReplyServerPacket::ForReplyCodeDefault(code, data). Throws std::invalid_argument if code has its own case

Integer cases without data don't get a factory, and values that are handled by the default case are created with the default factory.

To read switch data, use the generated As<Case>() accessors. They return a pointer to the case data, or nullptr if the variant holds the data for a different case, so there is no need to check the switch field as well:

if (const auto* data = init.AsOk())
{
// use data->player_id
}
else if (const auto* banned = init.AsBanned())
{
if (const auto* temporary = banned->AsTemporary())
{
// use temporary->minutes_remaining
}
}
Reading and writing EO data.

Accessors are named like the factories, but only for one switch: As<Value>, As<CaseClass> for integer cases and As<Field>Default for the default case.

Pub and map files

std::ifstream file("dat001.eif", std::ios::binary);
std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
data::EoReader eif_reader(bytes);
eif.Deserialize(eif_reader);
for (const auto& item : eif.items)
{
std::cout << item.name << "\n";
}
Endless Item File.
Definition structs.hpp:222
void Deserialize(data::EoReader &reader) override
Deserializes this object from the provided reader, replacing its current data.
std::vector< EifRecord > items
Definition structs.hpp:237

protocol::pub::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 in place. Each function accepts a std::vector<std::uint8_t>&, a std::string&, or a pointer and length:

// Encrypt
DataEncrypter::SwapMultiples(bytes, 6);
DataEncrypter::Interleave(bytes);
DataEncrypter::FlipMsb(bytes);
// Decrypt
DataEncrypter::FlipMsb(bytes);
DataEncrypter::Deinterleave(bytes);
DataEncrypter::SwapMultiples(bytes, 6);
Functions for encrypting and decrypting EO data.
Definition data_encrypter.hpp:21

A server generates a sequence start for the init handshake and tracks the sequence for each connection:

init_ok.seq1 = start.Seq1();
init_ok.seq2 = start.Seq2();
packet::PacketSequencer sequencer(start);
int expected_sequence = sequencer.NextSequence();
static InitSequenceStart Generate(Urbg &rng)
Generates an instance with a random value in the range [0, MaxValue).
Definition sequence_start.hpp:85
A class for generating packet sequences.
Definition packet_sequencer.hpp:11

ServerVerifier::Hash(challenge) computes the response to the client's challenge in InitInitClientPacket.

Errors

  • EoReader does not throw when data runs out. Reads return 0 or empty values, like the official client.
  • EoWriter throws std::invalid_argument for values that can't be encoded.
  • Generated Serialize methods throw eolib::SerializationError for invalid objects, such as fixed-size arrays with the wrong number of elements or switch data that doesn't match the switch field.
  • All eolib exceptions derive from eolib::EolibError.

Windows

The public headers compile after <windows.h> without NOMINMAX or WIN32_LEAN_AND_MEAN; a Windows test checks this. Members named like Win32 macros (for example Weight::max) are not followed by (, so function-like macros do not expand. Object-like macros from other headers can still collide with enum values. If that happens, include eolib before those headers or #undef the macro.