Loading...   

[Show Table of Contents]


§Architectural Notes

§Function Flow Produciton

  • Right here the functional architecture and flow of websockets will be described

§Function Flow (A-Z)

The core web socket protocol designed for EQEmu has been written in mind for extreme flexibility and not needing to manipulate redundant code.

All processes (web_interface, world and zone) use handler registers for their each and own respective projects.

  • web_interface/web_interface.cpp
  • The very first point in which a client request is processed is at int callback_eqemu under case LWS_CALLBACK_RECEIVE in which the json request is processed and finally dispatched to the appropriate server action at (web_interface)
auto call_func = iter->second.second;
call_func(session, document, method);
  • int main() {} registers all methods through the use of register_methods(); (web_interface)
void register_methods()
{
	register_authorized_methods();
	register_unauthorized_methods();
}
  • The unauthorized method is simply the initial authorization of the web client so that we don't get people hacking or hijacking servers without proper authorization. Most of the functions are made under register_authorized_methods(); (web_interface)
void register_authorized_methods()
{
	authorized_methods["WebInterface.Authorize"] = std::make_pair(0, handle_method_token_auth);
	authorized_methods["World.ListZones"] = std::make_pair(10, handle_method_no_args);
	authorized_methods["World.GetZoneDetails"] = std::make_pair(10, handle_method_get_zone_info);
	authorized_methods["Zone.Subscribe"] = std::make_pair(10, handle_method_subscribe);
	authorized_methods["Zone.Unsubscribe"] = std::make_pair(10, handle_method_subscribe);
}
  • The authorized_methods is a std::map that stores the methods and what functions they are tied to.

§Browser Client -> Web Interface -> World -> Web Interface -> Browser Client Example with (World.GetZoneDetails)

  • The first parameter of std::make_pair is the authorization level once fully implemented, and the second parameter is the function handler, the method is matched up from the call made from the initial JSON client browser request:
var str = JSON.stringify({id: 'get_zone_info_id', method: 'World.GetZoneDetails', params: [obj.result[key]]});
socket.send(str);
  • web_interface/web_interface.cpp finds out what method this is and eventually dispatches it via:
auto iter = authorized_methods.find(method);
auto call_func = iter->second.second;
call_func(session, document, method);

/* Which then makes a reference to header file for */

std::map> authorized_methods;
  • You can see this makes a reference to authorized_methods which is the std::map that was initialized on web_interface bootup, call_func essentially is directly executing the function handler that is bound to that std::map object member. In this example we were using 'World.GetZoneDetails' so it would have processed this code that was initialized on bootup:
authorized_methods["World.GetZoneDetails"] = std::make_pair(10, handle_method_get_zone_info);
  • Which the function looks like:
void handle_method_get_zone_info(per_session_data_eqemu *session, rapidjson::Document &document, std::string &method)
{
	CheckParams(1, "[zoneserver_id]");
	VerifyID();
	CalculateSize();

	ServerPacket *pack = new ServerPacket(ServerOP_WIRemoteCall, sz);
	pack->WriteUInt32((uint32)id.size());
	pack->WriteString(id.c_str());
	pack->WriteUInt32((uint32)session->uuid.size());
	pack->WriteString(session->uuid.c_str());
	pack->WriteUInt32((uint32)method.size());
	pack->WriteString(method.c_str());
	pack->WriteUInt32(1);

	auto ¶ms = document["params"];
	auto ¶m = params[(rapidjson::SizeType)0];
	pack->WriteUInt32((uint32)strlen(param.GetString()));
	pack->WriteString(param.GetString());
	worldserver->SendPacket(pack);
	safe_delete(pack);
}
  • At this point this is where the client data LEAVES web_interface and makes its way to world
  • So under world/web_interface.cpp under function 'bool WebInterfaceConnection::Process()' is where we process packets between web_interface and world, you will notice that there is a switch statement in which we are looking for the opcode 'ServerOP_WIRemoteCall' because that ise what we sent our packet opcode (header if you will) in the function to relay data.
  • Currently that case statement looks something like:
case ServerOP_WIRemoteCall:
{
	char *id = nullptr;
	char *session_id = nullptr;
	char *method = nullptr;

	id = new char[pack->ReadUInt32() + 1];
	pack->ReadString(id);

	session_id = new char[pack->ReadUInt32() + 1];
	pack->ReadString(session_id);

	method = new char[pack->ReadUInt32() + 1];
	pack->ReadString(method);

	uint32 param_count = pack->ReadUInt32();
	std::vector params;
	for(uint32 i = 0; i < param_count; ++i) {
		char *p = new char[pack->ReadUInt32() + 1];
		pack->ReadString(p);
		params.push_back(p);
		safe_delete_array(p);
	}

	if (remote_call_methods.count(method) != 0) {
		auto f = remote_call_methods[method];
		f(method, session_id, id, params);
	}

	safe_delete_array(id);
	safe_delete_array(session_id);
	safe_delete_array(method);
	break;
}
  • This does a few things:
    • It breaks apart the id (Call Identifier) in this case it is World.GetZoneDetails
    • session_id - This is the UUID that the client is assigned automatically by the server during connection intialization
    • method  = Is the method handler that we want to reference out of the std::map (Memory bank) of world's own remote_call_methods
  • Right away the worldserver takes the method string and kicks it off to the handlers for world which currently looks like: (world/remote_call.cpp)
void register_remote_call_handlers() {
	remote_call_methods["World.ListZones"] = handle_rc_list_zones;
	remote_call_methods["World.GetZoneDetails"] = handle_rc_get_zone_info;
	remote_call_methods["Zone.Subscribe"] = handle_rc_relay;
	remote_call_methods["Zone.Unsubscribe"] = handle_rc_relay;
}
  • As you can see it is mapped to function handle_rc_get_zone_info
void handle_rc_get_zone_info(const std::string &method, const std::string &connection_id, const std::string &request_id, const std::vector ¶ms) {
	std::string error;
	std::map res;
	if(params.size() != 1) {
		error = "Expected only one zone_id.";
		RemoteCallResponse(connection_id, request_id, res, error);
		return;
	}

	ZoneServer *zs = zoneserver_list.FindByID(atoi(params[0].c_str()));
	if(zs == nullptr) {
		error = "Invalid zone";
		RemoteCallResponse(connection_id, request_id, res, error);
		return;
	}

	res["type"] = zs->IsStaticZone() ? "static" : "dynamic";
	res["zone_id"] = itoa(zs->GetZoneID());
	res["instance_id"] = itoa(zs->GetInstanceID());
	res["launch_name"] = zs->GetLaunchName();
	res["launched_name"] = zs->GetLaunchedName();
	res["short_name"] = zs->GetZoneName();
	res["long_name"] = zs->GetZoneLongName();
	res["port"] = itoa(zs->GetCPort());
	res["player_count"] = itoa(zs->NumPlayers());
	RemoteCallResponse(connection_id, request_id, res, error);
}
  • Which will fetch information from a specific zone process based on the parameters sent from the browser client
  • NOTE: Since this specific call is only getting zone specific information that is only required from world process, we do NOT talk to the zone process, we immediately start making our way back to the browser client, with function RemoteCallResponse

world/remote_call.cpp

void RemoteCallResponse(const std::string &connection_id, const std::string &request_id, const std::map &res, const std::string &error) {
	uint32 sz = (uint32)(connection_id.size() + request_id.size() + error.size() + 3 + 16);
	auto iter = res.begin();
	while(iter != res.end()) {
		sz += (uint32)iter->first.size() + (uint32)iter->second.size() + 10;
		++iter;
	}

	ServerPacket *pack = new ServerPacket(ServerOP_WIRemoteCallResponse, sz);
	pack->WriteUInt32((uint32)request_id.size());
	pack->WriteString(request_id.c_str());
	pack->WriteUInt32((uint32)connection_id.size());
	pack->WriteString(connection_id.c_str());
	pack->WriteUInt32((uint32)error.size());
	pack->WriteString(error.c_str());
	pack->WriteUInt32((uint32)res.size());
	iter = res.begin();
	while(iter != res.end()) {
		pack->WriteUInt32((uint32)iter->first.size());
		pack->WriteString(iter->first.c_str());
		pack->WriteUInt32((uint32)iter->second.size());
		pack->WriteString(iter->second.c_str());
		++iter;
	}

	WILink.SendPacket(pack); /* Send packet to Web Interface */
	safe_delete(pack);
}
  • WILink.SendPacket is going to send a packet back to web interface so at this point we will be looking for the opcode ServerOP_WIRemoteCallResponse in the cpp file where World -> Web Interface communication is established:
case ServerOP_WIRemoteCallResponse: {
	char *id = nullptr;
	char *session_id = nullptr;
	char *error = nullptr;

	id = new char[pack->ReadUInt32() + 1];
	pack->ReadString(id);

	session_id = new char[pack->ReadUInt32() + 1];
	pack->ReadString(session_id);

	error = new char[pack->ReadUInt32() + 1];
	pack->ReadString(error);

	uint32 param_count = pack->ReadUInt32();
	std::map params;
	for(uint32 i = 0; i < param_count; ++i) {
		char *first = new char[pack->ReadUInt32() + 1];
		pack->ReadString(first);

		char *second = new char[pack->ReadUInt32() + 1];
		pack->ReadString(second);

		params[first] = second;

		safe_delete_array(first);
		safe_delete_array(second);
	}

	//send the response to client...
	rapidjson::StringBuffer s;
	rapidjson::Writer writer(s);

	writer.StartObject();
	writer.String("id");
	if(strlen(id) == 0) {
		writer.Null();
	} else {
		writer.String(id);
	}

	if(strlen(error) != 0) {
		writer.String("error");
		writer.Bool(true);

		writer.String("result");
		writer.String(error);
	} else {
		writer.String("error");
		writer.Null();
		writer.String("result");
		writer.StartObject();
		auto iter = params.begin();
		while(iter != params.end()) {
			writer.String(iter->first.c_str());
			writer.String(iter->second.c_str());
			++iter;
		}
		writer.EndObject();
	}
	writer.EndObject();

	if(sessions.count(session_id) != 0) {
		per_session_data_eqemu *session = sessions[session_id];
		session->send_queue->push_back(s.GetString());
	}

	safe_delete_array(id);
	safe_delete_array(session_id);
	safe_delete_array(error);
	break;
}
  • As this picks apart the parameters of the packet, it will send the data back to the appropriate web browser client session based on the UUID that was passed through the packet relay:
  • per_session_data_eqemu *session = sessions[session_id];
    session->send_queue->push_back(s.GetString());
  • At this point, the client sees the response:

Use Cases

  • Immediately I can see building a very extremely plain and simple HTML file with the build folder with a simple javascript file that will actually display and show all of the API documented right in the file as I establish it. As I get each function working, it will have a way of performing examples right in the HTML file itself and you would be able to actually have quite a bit of server administration control right from a simple HTML file
  • The idea is that any data sent from the server would be sent using serialized output
  • All transactions to the server use a token in the request otherwise all actions are dropped
  • Once this is working on socket_server.exe I will think about how I want to structure all messages sent up to the server so everything is as simple and flexible as possible so there is no crappy amount of redundant messages. I actually want to go back with queryserv and write a generic query logging system that would not require excessive structures and opcodes, instead send a variable sized query that can be dispatched from any process (Another tangent)
  • Anything that is leveraged to use the socket server, would only send IF socket server was UP and there was an active streaming javascript client
  • You could use this to do an incredible amount of things, and I believe very easily that a flexible API can be built very quickly, this is why I am so amped to get this setup
    • 2D Map Viewer
      • Anytime position updates are sent in a zone, send a serialized position update only when a socket server is alive and there is currently a client listening, would measure this through states
      • This could be incredibly expanded upon, being able to select NPC's or PC's, seeing either's hatelist and/or buffs. Manipulating zone states via any available in game command that would be manipulated in a controlled recieving end of the zone servers
    • General
      • HUGE development possibilities
      • HUGE management possibilities
      • First, obtaining a list of zones from the worldserver, so that any communication call can target specific zone processes
      • Getting Zone Up/Down Status, how long a zone has been up, how many NPC's are in the zone and all sorts of other statistics
      • Locking/Unlocking zones
      • Shutting a zone process down
      • Sending zone based shouts
      • Repop zone
      • Reload quests in zone
      • View the zone logs realtime
      • Potentially hook all queries happening in the zone (Debugging) 
      • Reloading any particular stateful thing via Zone:: API
      • Reloading Doors
      • Moving a player to a particular part of a zone
      • Reloading Zone Config
      • Setting Zone sky parameters realtime (Editing)
      • Messing with weather in realtime javascript sliders
      • Set zone Time
      • Manipulate Task states on characters
      • Viewing of particular memory sets in a zone
      • Casting spells on players
      • Moving NPC's and leveraging serverside Get Best Z via dragging and dropping elements on a 2D web map
      • Make it easier to manipulate NPC's for GM events, using the 2D web map to tell an NPC where to path to realtime
      • Adding player to guild
      • Creating a guild
      • Hooking native #commands
      • Send broadcast messages
      • Leveraging entity specific functions
      • Getting a NPC or PC's target realtime
      • Force zone a PC
      • Manipulate NPC loottable realtime (GM Event)
      • Add item to PC cursor