Client programmer's guide
Abstraction layers
The agent layer
Item referencing
The client abstraction layer
LocalMainAgent
Tile
TileDictionary
ClientContext
Multiple clients
Bootstrapping the client
This document describes the system design from a client-side perspective,
i.e what needs to be understood in order to program White Orb clients.
Abstraction layers
This is an overall picture of which layers are involved in the client/server
design.
-
Server: this is the actual game world. I won't talk any more
about the server itself in this document.
-
Agent layer: this layer allows external clients to communicate with
the server via so-called "agents" (a dumb name, yes I know). The
agents will watch what happens in the world and report to the client when
necessary. It will also act as an intermediary allowing the client
to act upon the world. All server-side optimization is taken care
of in this layer, so that the server layer does not have to worry about
internet-related matters at all. The methods provided by the Agent
layer are created in such a way to minimize internet traffic.
-
Client abstraction layer: This is a set of classes designed to make
life easier for the client programmer. More specifically, it provides
more higher level methods than the agent layer and does all sorts of optimization
and caching to minimize internet traffic. The goal is to save the
client implementation programmer from having to worry about networking
matters at all.
-
Client implementation: This is where the actual client is built.
The client implementation code uses the client abstraction layer to communicate
with the server on a high level of abstraction, thus not having to worry
about networking performance matters. It is possible to design totally
different clients that still use the same client abstraction layer
The Agent layer
As I mentioned above, the client implementation layer does not need to
speak directly to the agent layer. But I'll describe it anyway -
it's good to know how the API interface looks like. The MainAgent
is the main interface for clients to use over the internet.
A MainAgent always has an actor and usually has a connected client.
The actor is the item in the server that the MainAgent represents,
sort of like the puppet that the client is actually acting on behalf of.
This is usually an item in the world, for example an Account or a Soul.
The actor is used as a basis for security - determining what actions are
allowed, and what information can be accessed. The client (see the
javadoc for MainClient)
is what the agent uses for callback purposes, for example reporting property
changes, server messages, and stuff like that.
The MainAgent interface provides a number of different types of methods
(see the MainAgent
javadoc):
-
Methods for manipulating the world, such as doAction, deleteItem,
createItem, etc.
-
Methods for watching the world, such as getItemProperties, getItemChildren,
watchEventsFired, etc. Note that most of these methods including
the option of caching the information. This means the agent
offers allows the client to "subscribe" to that information from the server,
and send updates to the client when necessary. So if you ask for
the name of an item, and set caching to true, then the MainAgent will also
notify the client when that item's name changes in the future.
-
Methods for removing caching, i.e "unsubscribe" to information that
the MainAgent previously has been asked to cache. (these have not
been implemented yet...)
-
Other methods. This includes:
-
setClient(...). A very important method. This is used to tell
the MainAgent which client it should talk back to (for example to report
cached property changes).
-
getAnotherMainAgent(...). This will spawn a new main agent, with
a different actor if desired. This is used when, for example, a player
opens up a player client for his character - a new MainAgent will be created
for that character's soul.
-
System methods such as getSystemProperty, getFreeMemory, etc.
So basically a MainAgent represents one client connection, whether it be
a player's account, a character, a builder, or an administrator. Each one
needs to be able to access the world and retrieve information about it.
There are a few other, more specialized agents - MapAgent
and AreaAgent.
These two may be merged together later, I'm not sure. AreaAgent provide
access to a specific area, and is designed to be connected to an AreaClient.
The AreaClient will receive updates when the contents of the area changes.
The MapAgent is similar to AreaAgent, but represents a mobile subset of
an area, for example a creature's field of view. Thus it includes
things like sight range. The corresponding MapClient
is similar to AreaClient, but also includes facilities for scrolling, i.e
the MapAgent will tell the MapClient when new squares are visible, and
when old squares move out off the edge of the map.
Item referencing
Items are constantly being referred to across all layers. The different
layers refer to items in the following ways when communicating with each
other:
If you study the diagram you can see that the main purpose of the client
abstraction layer and Agent layer is to translate the server's Item-based
world to the client's Tile-based world.
Let's take a concrete example. The server tells the client that
it sees a creature, and now the client (being a builder) wants to delete
that creature. The server layer tells the agent layer using a normal
Item (or, more specifically, a Creature). Since Items cannot be sent
over the net (they are too heavily tied into the server world) the agent
creates a corresponding ItemReference and stores the Item's integer id
for further reference. The ItemReference encapsulates this integer
id, plus some cached properties (for example the item's icon and location).
This ItemReference is sent to the client.
The client abstraction layer receives this, and creates a corresponding
Tile object. The client can communicate with Tile objects in a very
easy fashion, without having to worry about client/server communication
and property caching. Now the client only wants to delete it, so
it tells the client abstraction layer to delete the Tile. The client
abstraction layer will, in turn, extract the item ID integer and tell the
agent layer to delete the item with that ID (since that is all the agent
needs to be able to look up the original Item).
The agent receive the integer ID, uses it to look up the corresponding
Item, and then deletes it. Thus internet traffic is minimized, without
complicating the implementation of the client or the server.
The client abstraction layer
The agent layer provides all the functionality needed by any client, but
the problem is that each single call to the agent layer must be sent through
the internet. Of course the actual mechanisms for remote method invocation
is not so difficult, using for example Voyager, but we still need to minimize
the internet traffic. The client implementation will be retrieving
very much information from the server, but in many cases this information
can be cached locally (if the client will retrieve the information more
often than the information is changed in the server). Thus, the client
abstraction layer provides a bunch of high-level classes that hide all
gory networking details from the client implementation.
This diagram shows the public classes within the client abstraction
layer, and how they relate to each other. The arrows represent dependencies
(i.e references). Red or dotted arrow means that the reference is
private, and cannot be traversed from outside that class. Thus the
client abstraction layer really hides the MainAgent from the client implementation.
LocalMainAgent
The API interface of LocalMainAgent looks very similar to MainAgent - but
the level of abstraction is raised. The main differences are:
-
Tiles are used to refer to items, instead of ItemReferences and ints.
-
All property retrieval and event subscription methods are removed.
Corresponding methods are provided by the Tile class instead, so the client
implementation feels that it is acting directly upon Tiles. Thus
pretty much all methods that need to refer to items are moved to the Tile
class instead.
-
Action tracking is made easier (the doAction methods) using ActionStatusListener
interfaces, instead of just integer IDs.
Tile
A Tile is a high-level representation of an item in the server. It
contains the following general types of methods:
-
Listener registration. Here objects can register themselves as listeners
and thus be notified when properties change, events occur, etc. This
includes client-side events such as selecting a Tile in the GUI.
-
Information retrieval. These methods let easily retrieve property
values and children of the Tile.
-
Caching. These methods allow you to specify which properties (and
other things) should be cached. Cached information is stored locally
and automatically updated from the server, so subsuquent retrievals of
that property value will be fast.
There is never (or should never be) more than one Tile representing a single
item.
TileDictionary
This class stores and indexes all Tiles. It provides the following
types of methods:
-
Search methods, for example to find all Tiles that match the name "jo*"
-
Tile creation/retrieval. When an ItemReference is to be wrapped into
a Tile, an existing Tile should be used if there is one representing that
item - if not, a new one should be created. The TileDictionary takes
care of this checking - you simply use getTile(ItemReference ref) to retreive
a corresponding Tile.
ClientContext
The ClientContext is what holds it all together, i.e it represents the
client abstraction layer as a whole. It contains a reference to the
LocalMainAgent as well as client-specific things like the status bar, the
currently selected tile, and such things. There are subclasses of
ClientContext that keep track of more information for specific types of
clients. PlayerClientContext, for example, contains info about which
player the client represents, BuilderClientContext contains info about
which item type is selected in the toolbox.
Any functionality that all clients are expected to provide should be
placed here, in a non GUI-specific way. General methods for error
handling and logging will probably be added here.
Multiple clients
In many cases a single person will be using multiple clients. There
are currently four basic client types: account, player, builder, and admin.
These reflect the different "roles" with which you can use the system.
-
Account client: allows you to create/destroy characters in your account,
and open new clients.
-
Player client: allows you to wander the world in the role of a character
-
Builder client: allows you to build and change stuff in the world
-
Admin client: allows you to do general system administration such as saving
the world (hmmm...), creating/destroying accounts, read logs, stop the
server, and stuff like that.
When more than one client is open, each client is completely independent.
This is to ensure that the roles and security restrictions don't interfere
with each other. Here is an example:
The client abstraction layers cannot be shared because they contain
state information (such as cached properties) that may vary between the
clients. In the Player client, for example, the character might be
drunk and have a corrupted view of the world.
Bootstrapping the client
So how does it all start? Well, the first thing the client must do
is connect to the server. This is done using Voyager's RPC implementation.
The Server provides the following three methods:
MainAgent login(String name, String password);
MainAgent loginAsGuest(String name);
MainAgent createAccount(String name, String password);
Once you manage to log in successfully, you will receive a MainAgent representing
your account (i.e your account object in the server will be the actor).
Now you can create a ClientContext (the constructor requires a MainAgent),
which in turn will create the LocalMainAgent, TileDictionary, and all that.
Once you have a ClientContext the client abstraction layer is complete.
Now you need to create a client, for example an account client (implemented
as AccountPanel). The AccountPanel takes the ClientContext as a parameter
and does the rest.
All this behaviour is already implemented - in OrbClient. I just
wanted to describe the general process...
Henrik
Kniberg
Last updated: