Project:
https://github.com/Zaela/EQNet
Main include file:
https://github.com/Zaela/EQNet/blob/...nclude/eqnet.h
Netcode talk in IRC lately inspired me to start this little project: a C API for connecting to EQEmu servers as a client.
The goal is to provide a relatively small set of functions to handle common client-to-server communications (connecting, moving around, sending chat messages, using skills, etc), with almost all of the messy networking details -- and client version differences -- handled transparently to the user.
For server-to-client communications, a simple event loop is provided to notify the host program of state changes (e.g. login -> char select, char select -> zone), failures (timeout, disconnection, zone unavailable) and, most importantly, incoming data packets.
For the login and char select/world connection states, the user doesn't need to worry about packets at all; the API takes care of the details and simply provides a small set of notifications and a few functions to retrieve structured data (info about characters at char select, for instance).
For the in-zone connection state, packets are inevitable. The ultimate goal is to provide a reduced, streamlined set of opcodes and packet structures that iron out most of the client-version-specific details (hint: most of the work to be done is here). In the meantime, though -- and for some extra flexibility and potential efficiency -- the "native", client-version-specific opcodes and packet data are also made available.
This is all a work in progress (just started a week ago, technically) and the API is far from complete or stable. However, at this point it should at least be possible to log in, go through server select and char select and finally reach a zone and get spammed with all the native packets under the sun as whatever client version you wish (Titanium, SoF, SoD, Underfoot, RoF and RoF2 have all been tested and confirmed to that point on local and remote servers -- although issues are certainly possible). It's possible that someone could find a use for it already -- functions are provided for sending arbitrary packets to the server, could be used for networking unit tests. Some basic work still needs to be done (ping, unexpected disconnection detection and recovery, handling for missed/misordered udp packets) but I've been making fairly steady progress thus far.
Very basic example usage:
Code:
#include "eqnet.h"
#include <cstdio>
#include <cstring>
#include <thread>
#include <chrono>
#ifdef _WIN32
#include <windows.h>
#endif
void HandleReceivedPacket(EQNet* net, EQNet_Event& ev);
int main(int argc, const char** argv)
{
const char* username = argv[1];
const char* password = argv[2];
const char* partialServerName = argv[3];
const char* characterName = argv[4];
#ifdef _WIN32
SetConsoleTitle("EQNet Example");
#endif
// EQNet_Init must be called exactly once before creating any EQNet objects
if (!EQNet_Init())
{
fprintf(stderr, "EQNet_Init failed.\n");
return 1;
}
// The EQNet object is an opaque pointer representing the overall state of one distinct
// "connection" or "session". One EQNet object will last you through login, char select,
// zone, and back again. EQNet objects are independent -- you can have multiple running
// in a single thread or in separate threads without worrying about interfering with
// each other.
EQNet* net = EQNet_Create();
// Set our desired client version to masquerade as
EQNet_SetClientVersion(net, EQNET_CLIENT_ReignOfFear);
// Jumpstart the event loop by attempting to log in to server select
EQNet_LoginToServerSelect(net, username, password);
const EQNet_Character* selectedChar = nullptr;
// Main loop
for (;;)
{
// Event loop!
EQNet_Event ev;
// A lot of magic happens in the EQNet_Poll call. It must be called regularly to keep our
// network I/O queues flowing. If you are in-zone or at character select and will be doing
// some time-consuming processing that won't allow you to return to your event loop for a
// while (multiple seconds), call EQNet_KeepAlive periodically let the server know you're
// still there
while (EQNet_Poll(net, &ev))
{
// Event handlers
switch (ev.type)
{
case EQNET_EVENT_FatalError:
fprintf(stderr, "EQNet encountered a fatal error: %s\n", EQNet_GetErrorMessage(net));
goto CLOSE;
case EQNET_EVENT_Timeout:
if (selectedChar)
{
// If we get a timeout trying to enter a zone, it may be booting up
// try connecting again (auto-reconnect attempt not yet implemented)
EQNet_WorldToZone(net, selectedChar);
selectedChar = nullptr;
break;
}
fprintf(stderr, "Connection timed out, aborting\n");
goto CLOSE;
case EQNET_EVENT_BadCredentials:
fprintf(stderr, "Invalid username/password\n");
goto CLOSE;
case EQNET_EVENT_AtServerSelect:
{
fprintf(stdout, "Reached Server Select\n");
// We can view the available servers now
int numServers = 0;
const EQNet_Server* servers = EQNet_GetServerList(net, &numServers);
// Find our desired server
for (int i = 0; i < numServers; ++i)
{
if (strstr(servers[i].name, partialServerName) == nullptr)
continue;
// Found it!
if (!EQNet_ServerIsUp(net, &servers[i]))
{
fprintf(stderr, "Server '%s' is %s, aborting\n", servers[i].name,
EQNet_ServerIsLocked(net, &servers[i]) ? "LOCKED" : "DOWN");
goto CLOSE;
}
// Send request to log in to this server
fprintf(stdout, "Attempting to connect to %s\n", servers[i].name);
EQNet_LoginToWorld(net, &servers[i]);
break;
}
break;
}
case EQNET_EVENT_WorldConnectFailed:
fprintf(stderr, "World server refused our connection, aborting\n");
goto CLOSE;
case EQNET_EVENT_LoginToWorld:
fprintf(stdout, "Connecting to world server . . .\n");
break;
case EQNET_EVENT_AtCharacterSelect:
{
fprintf(stdout, "Reached Character Select\n");
// We can view our available characters now
int numChars = 0;
const EQNet_Character* chars = EQNet_GetCharacterList(net, &numChars);
// Find our desired character
for (int i = 0; i < numChars; ++i)
{
if (strcmp(characterName, chars[i].name) != 0)
continue;
// Found them! Send request to zone them in
EQNet_WorldToZone(net, &chars[i]);
selectedChar = &chars[i];
break;
}
break;
}
case EQNET_EVENT_ZoneUnavailable:
fprintf(stderr, "Zone unavailable, aborting\n");
goto CLOSE;
case EQNET_EVENT_Zoning:
fprintf(stdout, "Connecting to zone . . .\n");
break;
case EQNET_EVENT_AtZone:
fprintf(stdout, "Connection to zone complete\n");
break;
case EQNET_EVENT_Packet:
// Note that some packets (e.g. MessageOfTheDay) are received before we officially leave character select
#ifndef NO_OPCODE_SPAM
fprintf(stdout, "Received packet: EQNet opcode 0x%0.4x, len %u; native opcode 0x%0.4x, len %u\n",
ev.packet.opcode, ev.packet.len, ev.nativePacket.opcode, ev.nativePacket.len);
#endif
HandleReceivedPacket(net, ev);
break;
} // switch
}
// Don't hog the CPU
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
CLOSE:
// Release our EQNet object
EQNet_Destroy(net);
EQNet_Close(); // Close the library
return 0;
}
void HandleReceivedPacket(EQNet* net, EQNet_Event& ev)
{
switch (ev.packet.opcode)
{
case EQNET_OP_MessageOfTheDay:
fprintf(stdout, "Message of the Day: %s\n", (const char*)ev.packet.data);
break;
case EQNET_OP_ChatMessage:
{
EQNetPacket_ChatMessage* msg = (EQNetPacket_ChatMessage*)ev.packet.data;
fprintf(stdout, "%s: %s\n", msg->senderName, msg->msg);
break;
}
case EQNET_OP_PlayerSpawn:
fprintf(stdout, "You spawned!\n");
EQNet_SendChatMessage(net, "I spawned!!");
break;
}
}
No real documentation yet.
Anyway, hope someone will find this interesting. If I find time to hop back on the "make my own client" bandwagon again I'll definitely put this to use, at least ;p