Hacking Cyphesis

From WorldForgeWiki
Jump to: navigation, search

This page lists a few tips and tricks for developers who want to learn how to modify Cyphesis to do their bidding. There are quite a few major parts to the WorldForge system that need to be understood in order to be able to confidently modify Cyphesis

The Atlas Library and Protocol

Atlas is the basic language used to converse between the clients (such as Ember or Sear) and Cyphesis.

Filters

Atlas data streams travel along a path of Filters (see Atlas/Filter.h). Each outgoing message is converted to a byte stream and piped through an optional chain of filters for compression or other transformations, then passed to a socket for transmission. Incoming messages are read from the socket, piped through the filters in the opposite direction and passed to a user specified Bridge (see Atlas/Bridge.h) callback class.

Bridges

The Bridge class presents an interface that accepts an Atlas stream. The beginning of a stream is indicated with a call to Bridge::streamBegin() and the end with a call to Bridge::streamEnd(). Between these two calls, the Bridge is said to be in stream context.

While the Bridge is in this stream context and we can send messages using Bridge::streamMessage(). Once Bridge::streamMessage() has been called, the Bridge is said to be in a map context. This means that the most basic data that can be transferred is a map of various other data. When a Bridge is in a map context, the various Bridge::map*Item() calls are allowed. These Bridge::map*Item() calls are similar to callbacks that are called on finding different tokens in a parser. To end the map, we finally call Bridge::mapEnd()

Note that the Bridge::map*Item() calls require a name as the value to "map" a piece of data to. For example, we can call:

long value = 1000;
streamBegin();

mapIntItem("test_integer", value);

mapEnd();
streamEnd();

The above code might output something like the following:

[                      // From streamBegin()
@test_integer=1000     // From mapIntItem()
]                      // From mapEnd()

So the string that gets sent over the network would be:

[@test_integer=1000]

The Bridge::list*Item() functions simply list the value of the data being passed without associating it with a specific name. For example,

long value = 1000;
streamBegin();

listIntItem(value);

mapEnd();
streamEnd();

would give the following message string:

[1000]

The above examples were taken from a specific implementation (Codecs/Packed.[h,cpp]) and the method of making calls to beginnings and closings of messages may be different for others.

Remember that Atlas supports the following data types:

  • Integers (eg. 0, 10, -40)
  • Float (eg. 1.0, -10.0)
  • Strings (eg "helloworld", "random_id", "my_name")
  • Lists (eg. [10, -1.0, "hello"])
  • Maps (eg. {name: "foo", height: 100} )

Codecs

A Codec is a type of Bridge that encodes and decodes between byte streams and structured data. An example of a Codec would be conversion between XML and byte stream.

The Codec class inherits from the Bridge class and provides another added function called poll(). Codec have encoder and decoder parts. They convert between structured data and byte stream. When a Codec is used to structure data, the poll() is called. In this function, you need to define what needs to be done to structure the data that is coming into the Codec (eg. one character at a time). You also need to define what the various map*Item() calls will do. A great example to understand how this works is in the XML Codec class (in Codecs/XML.[h,cpp]) or the Packed Codec class (in Codecs/Packed.[h,cpp]).

This article here by Al Riddoch and James Turner explains more about the structure of Atlas data.

Atlas already provides useful Codecs to get the job done for most situations (XML, Packed data, Bach etc. in the Codecs subfolder). If you are extremely picky about your data protocol (perhaps you want a binary optimized stream), you can write a new Codec following the guidelines above in the Bridge section and by studying the various available Codecs.

Note: The implementation makes use of iostream to allow for writing to any type of standard stream (network, stdout, string etc.). This mean you simply have to define what the output to the stream should be. The type of stream itself will be set by the Atlas::Objects::ObjectsEncoder and Atlas::Objects::ObjectsDecoder classes (in Atlas/Objects/[Encoder,Decoder].[h,cpp]). These classes add the incoming data into the object heirarchy by extracting their data or send over the network by calling the appropriate Codec functions. For example, Cyphesis uses a subclass of ObjectsDecoder to decode incoming messages (it uses the same subclass to encode using the same codec).

Atlas Object Hierarchy

Root Data

Atlas has a basic type of data called RootData (defined in Atlas/Objects/Root.[h,cpp]). This is the base class for any kind of data that is transmitted across the network. All the data is defined using attributes for each object. For example, each object might have an "id" attribute (of type string) that holds the unique ID of the object in the world.

There are two core types of data that are transmitted across the network. These derive from RootData and are:

  • Operations (defined in Atlas/Objects/RootOperation.[h,cpp])
  • Entities (defined in Atlas/Objects/RootEntity.[h,cpp])

Note: These files are very well documented so you should really look into them for further details on the various functions available for each of them.

Atlas Operations

Operations are basically actions that can be sent across the network encoded as Atlas data. You can create objects for various types of operations (these need to be compiled in and currently and cannot be created dynamically from what I know) and when received at the destination, the appropriate handler function will be called.

For example in Cyphesis, sending a "Create" operation (to create new entities in the game world) will result in Account::operation() being called which in turn calls Account::CreateOperation which creates the required entity. This routing to the Account::operation() function is a nifty feature as it allows you to define privilege levels very easily. For example, to create an account type that doesn't have the privilege of creating entities in the game world, you can subclass Account into a Player class and overload the Account::operation() function to ignore Create operations! Take a look at the various Account subclasses in Cyphesis to understand this better (cyphesis/server/Account.[h,cpp] defines the basic account with various handlers. cyphesis/server/Admin.[h,cpp] defines an administrator account type with escalated privileges and cyphesis/server/Player.[h,cpp] defines a regular player account with lowered privileges).

Atlas Entities

Entities are any sort of in-game objects. Anything you see within the game world is an entity with a specific set of defining data. Player characters are entities, trees in the game are entities, objects on the floor are entities. Quite simple really.

Atlas Negotiation

When an client connects to a server or a server connects to a peer server, a process of negotiation takes place. Since both sides have to understand the data sent, the must also use the same Codecs to encode and decode data. This agreement of using the same Codecs and Filters (unimplemented?) takes place in the negotiation process (see Atlas/Negotiate.[h,cpp]).

The classes that help facilitate the negotiation on both sides are StreamAccept for servers and StreamConnect for clients (defined in Atlas/Net/Stream.[h,cpp]). These are simply subclasses of the Negotiate class and facilitate negotiation.

StreamAccept is used on the server once a stream connection has been established by a client. This class listens to the list of Atlas::Codec types that the client offers, and then responds with the name of the Atlas::Codec which it thinks is most suitable. Once the server has told the client which Atlas::Codec to use, negotiation is flagged as complete, and this object can be deleted.

StreamConnect is used once a stream connection has been established to the server. This class offers the server a list of Atlas::Codec types that the client can understand, and then listens for the servers decision. Once the server has told the client which Atlas::Codec to use, negotiation is flagged as complete, and this object can be deleted.

An example of this is in the CommClient class in Cyphesis (see the two constructors of CommClient in cyphesis/server/CommClient.[h,cpp]). The CommClient objects in Cyphesis are responsible for managing network communication with clients. Take a look at the CommClient::setup() and CommClient::negotiate() functions for a better idea of how negotiation works.

Some points that may be noted regarding negotiations:

  • It takes a small amount of time for a negotiation to complete.
  • Until the negotiate is complete no transmission of data should be done as the codec would not be set.
  • The negotiation object is normally deleted after negotiation is completed.