|
Client/Server Protocol
This page documents the Wyvern Client/Server bi-directional network
protocol.
Contents
The Wyvern client/server protocol has the following basic
characteristics:
-
It's a low-level, TCP socket-based, custom RPC protocol.
-
Data input and output occurs over the same socket connection.
-
The client always sends text/UTF-8 data to the server.
-
The server sends binary and UTF-8 data to the client, which
the client must decode manually.
-
Most binary data coming from the server is compressed using
the gzip format. It's compressed on the server side
using a java.util.zip.GZipOutputStream. You'll need access
to a library that provides you the ability to uncompress
(gunzip) the data stream on the client side.
-
The connection is not secure, and doesn't use SSL
or any form of encryption. Passwords are sent as text,
encrypted with the crypt() command in the standard C
library. You'll need to obtain access to a crypt()
function for your platform, in order for your login
information to be accepted by the server. You do not
need to be a
Wizard character
to develop a Wyvern game client.
-
The server writes int/short/byte values as high-byte
first. So reading an int, you'd read the high byte
first, and the low byte last.
-
All strings are passed in UTF-8 encoding. The first
2 bytes is a short value representing the length of
the string data.
The information passed back and forth consists of:
-
initial handshaking data to get the player logged in
-
text commands and their arguments from the client
-
RPC commands from the server, consisting of a one-byte code
used to determine which method to call on the client, followed
by a method-specific stream of bytes to decode.
For example, the server might send the byte value
INV_ADD, which means "add an item to the player's inventory view",
followed by the item's name (utf-8), popup menu commands (utf-8),
and image tile number, list index, and image (x,y) panning
offset, the latter three of which are 16-bit unsigned short
(int) values.
Because the protocol is TCP, there are no timeouts or retries
built into the protocol. If the socket is closed, the client
should disconnect and show the user the "connect to game" UI.
For the rest of this document, we're going to use the
following naming conventions:
Any ALL_CAPS_WITH_UNDERSCORES is an RPC Function Code. It's
a one-byte code that the server sends, to tell the client which
function it's invoking.
An RPC Function Code is usually followed by some data, and
this data will be called the message data for that
RPC function. The data is a essentially a sequence of bytes,
sometimes gzip-compressed, that needs to be decoded into
one of (usually) these four types:
-
a single primitive-type int value (32-bit int, 16-bit short,
or 8-bit byte). Sometimes these values are treated as
signed, and sometimes as unsigned. The formal protocol
definition shows how to decode and interpret each
function's data stream.
-
a utf8 text string, where the first two bytes is
the length of the string. The strings are not
null terminated.
-
an array of bytes, shorts, or ints
-
a multi-dimensional array of bytes, shorts, or ints
-
a composite message consisting of any combination of
the types above.
For instance, the message data for the INV_REMOVE function is simply a
16-bit short value saying which index to remove from the inventory
view. The INV_MODIFY function has more data: 2 UTF-8 strings,
followed by three 16-bit short values. The ZIP_SCREEN and
ZIP_PARTIAL_SCREEN functions have fairly complex message data formats
that require several stages of decoding and interpretation. They are
all described in detail in the formal protocol definition, below.
For reference, the data is always encoded on the server end
using a java.io.DataOutputStream writing to a
java.io.ByteArrayOutputStream. You can consult Sun's
Java 2 Platform
APIs (JDK 1.4) if you'd like more information about these
I/O classes.
The client should open a TCP socket to the host specified
by the player, or "opal.cabochon.com" by default. Note: as
of Jan 10 2003, opal is the only game server online. There
is currently no way to run a local copy of the game server,
so you'll have to do your testing by connecting to our
production servers. We will try to offer a test server at
some point.
The graphical-client port number is 2222. You should ensure
that your local firewall lets traffic through on this port.
You can tell if the game server is up in 3 ways:
-
You can telnet to the game server, port 2000, using any
standard telnet program. You don't have to have a
character to do this, although it won't let you log in
without one. However, if you connect with telnet
(e.g. "telnet opal.cabochon.com 2000"), you should get
a text-only welcome message and a login prompt from the
server. If your connection is refused, the server is
down (for maintenance or a scheduled reboot.)
-
You can
visit this URL
to see if there are people playing. If so, the server is up.
-
You can connect to the game using an existing graphical
client (usually the
Java Wyvern Client). If you don't have Java (JDK 1.4)
on your platform, you'll be flying somewhat blind, so we
recommend that you have a Windows or Linux PC available
for running the standard client.
In order to test most of the functionality of your client,
you'll need to have a character on Wyvern. You can create one
(entirely free of charge) using our
New User Signup Form.
Your password will be emailed to you by the
server.
Once you've established that the game server is available, you
can start writing the code for the handshake.
The handshake is a short exchange of data:
- The client sends some registration data to the server:
- an int whose value is 1 (REGISTER_EXISTING_PLAYER)
- two short values: the view width and height
being requested by the client. They should be
positive, odd numbers, and the server will
limit them to some preset value (currently 13).
- a UTF string: the player's name.
- a UTF string: the player's encrypted password
- a UTF string: the client version. This is
in one of two formats:
- client-type version-number
(e.g. "Java WebStart Client 1.1")
- PDA handheld-name version-number
(e.g. "PDA IPAQ PocketPC 2.2")
The client type is mostly for logging purposes,
so we can see what kinds of clients are connecting
to the game. PDA clients get some special handling,
as well - the player goes to a different starting
area, and there are other minor differences in the
gameplay. The protocol is the same for both.
Note that the protocol sends and receives UTF8 strings
as 2 bytes (the length of the data) followed by the
UTF8-encoded data. Most UTF8 strings sent from the
server are preceded by a 1-byte code specifying the
text style to draw the text in. This part of the
protocol will be changing in the near future.
- The server responds with a packet:
- an int: the packet header. All packets
from the server have an int packet header,
described below.
- an int: the major and minor protocol version
numbers that the server is using.
- a byte: LOGIN_SUCCESS (value=3), or
LOGIN_FAILURE (value=4).
- On LOGIN_SUCCESS, the handshake is finished,
and it's the end of the packet.
- On LOGIN_FAILURE, the packet includes a
style byte and a UTF8 error message.
The server will follow the packet with
a QUIT packet, described below.
After that, the server will begin sending data in packets,
but they won't necessarily come in any particular order.
After the handshake data is complete, the client should begin
polling the socket for data from the server. The client must
also respond to user input to send commands to the server, over
the same socket. Reads and writes can be arbitrarily interleaved;
the protocol is stateless after the handshake completes.
After the initial handshake is complete, the client can begin
sending text commands to the server. The commands fall into three
categories:
-
Input typed directly by the user, such as
get all from corpse
-
Mouse clicks, which are translated into text strings like
mouse 7 13 or
inv 3 247.
These are described below.
-
Special commands sent by the client to request specific
server functions, such as asking for an image or sound
clip to be dowloaded, or asking for a full-screen refresh.
All input from client-side UI objects is converted to commands,
as if the player had typed the command at the command-line.
The server expects the client data to be UTF-8 encoded. The
server first reads a 2-byte value that contains the length of
the UTF8 data. Then it reads the data. There is no null
terminator.
The gory details follow.
Generally, you send exactly what the user types into the
text-input field directly to the server as a utf8 string.
The client should limit the length of the input to
approximately 2000 characters. If the server receives
too much input from a client in one read, it will
deliberately close the connection.
When the user clicks the mouse in the map, the client should
generate a text command of the
form mouse x y modifiers,
where:
-
x is the viewport x "tile" coordinate where the
user clicked. The client viewport should ideally be
evenly divisible by 32 pixels, since the image ("tile")
size for the game is 32x32 pixels. If the user clicks
the mouse in the upper-left corner of the viewport,
it would be location (0, 0), with x increasing to the
right and y increasing downwards.
-
y is the viewport y tile coordinate. You typically
compute x and y by diving the pixel (x,y) locations of
the mouse press by 32.
-
modifiers is a numeric string whose int value
is a bitfield, with bit representing the Control, Alt
and/or Shift keys, and mouse buttons 1, 2 and 3.
The flags that the server looks for are:
shift key: 0000 0001 (0x00000001) "1"
ctrl key: 0000 0010 (0x00000002) "2"
alt key: 0000 1000 (0x00000008) "8"
button 1: 0001 0000 (0x00000010) "16"
button 2: 0000 1000 (0x00000008) "8"
button 3: 0000 0100 (0x00000004) "4"
Where, of course, the shift-key bit is set if the user
was holding the Shift key down when the mouse press
occurred, and so on. You should || (or) in each bit,
convert to a decimal integer, and then to a string.
For example, shift + left mouse button (button1) is
1 + 16, so you'd send 17 for the modifiers.
The ramification of this numbering scheme, described
in more detail below, is that there's no way to specify
two combinations: ctrl+button3, and alt+button2.
See the sel command for how the
mouse is handled when you click in the ground or inventory
views.
The client needs to send a handful of special commands as
the need arises:
-
#tile - asks the server to send the specified
tile mapping. Argument is the tile number for which
we need a mapping. Example: #tile 2117
The client should send this command when it gets
a request to draw a tile that isn't in the client's
tile-mappings hash (meaning server hasn't sent it yet.)
-
#img - sends a request to the server to send
us a particular image. Argument is the path to the
image to download. Example: #img monsters/goblin/orc
You send this command when you've received a tile
number from the server for an image whose timestamp
is older than the server timestamp for that image.
- #view - tells the server to change the size of
the player's camera to a new width and height, in
map coordinates (or viewport "tile" coordinates).
Example: #view 7 5 is sent by the PocketPC client
when the user wants to shrink the map from 7x7 and
show more of the text-output window.
- refresh - instructs the server to send a full-screen
refresh. There are no arguments - just send refresh.
Use this when the client's view size changes, or when
anything happens to cause the client to forget how to
draw the current map contents.
- sel - sent when the user chooses a menu command
for an item in the ground or inventory view. This command
is described below.
- player-visible commands - the client can, at its
option, offer UI shortcuts for player commands. For
instance, the default Java client has a row of icon buttons
that send commands like who, score, and so on. The
full list of player commands is available in the
Player Command Reference.
The client should send the sel command to the server
when the user clicks in the ground or inventory view.
Command arguments:
sel [gnd|inv] <index> [modifiers|command]
Where:
- gnd or inv specifies which view was chosen
- index is the index of the item selected (0-indexed)
- modifiers is an int value consisting of flags
specifying whether the ctrl, alt, and/or shift keys
were held down when the user clicked the mouse, as well
as which mouse button it was. The flags recognized by
the server are:
- shift mask: 0x00000001 (1)
- ctrl mask: 0x00000010 (2)
- alt mask: 0x00001000 (8)
- button1 mask (left): 0x00010000 (16)
- button2 mask (middle): 0x00001000 (8)
- button3 mask (right): 0x00000100 (4)
Note that alt-mask is the same as button2-mask - this
is for historical reasons (they were the same in older
versions of Java). So holding the alt key down emulates
the middle mouse button.
The modifiers are logically OR'ed together, and the int
value is converted to a decimal number as the argument.
For instance, if the user isn't holding down any keys,
and presses the left button, the modifiers argument is "16".
If the user holds down the Shift and Alt keys, and presses
the left mouse button, the modifiers argument is 16 + 8 + 1
= "25".
-
command a one-word command verb, such as drop,
loot, get,
or apply. The verbs that are
valid for a given item are sent from the server in the INV_ADD,
INV_MODIFY, and INV_RESEND RPC calls.
The client should send either the event modifiers or
a command, not both. It's up to the client to decide how to
map the mouse buttons - it's a good idea to create UI that
lets the user decide. For instance, in the default Java client,
the right mouse button brings up a menu containing the command
verbs, and the left button examines an item (configurable by
the player using the
mousebind
command, since the binding is stored on the server side.)
An Apple Macintosh client, in contrast, might opt to bring up the
command menu when pressing the single mouse button, and emulate the
shift, ctrl and alt key codes when the user presses some appropriate
key on the Mac keyboard.
The modifiers argument is designed rather poorly; it relies
on the actual input event mask values specified by Java, and
the values have changed from release to release. At some point
we'll add an entirely new set of flags that unambiguously
differentiate between the modifier keys and the mouse buttons.
The current flags will continue to be recognized by the server,
for compatibility with older clients.
Similarly, the mouse customization level available to players
is currently somewhat restrictive. We will eventually add the
ability to bind logical keys ("shift", "alt", "ctrl") and
mouse buttons to actions in the game. There are valid arguments
for doing this on the client or the server. Saving the mapping on
the server side means it applies no matter which computer
you're playing the game from; saving it on the client means
your mappings are valid for all of your characters. We'll
undoubtedly offer a server-side binding mechanism, and leave
it up to the client implementors to decide whether to offer
a client-side binding as well.
Every packet sent by the server, including the handshake
packet, starts with a 4-byte integer header:
-
The high-order byte is an RPC code. The codes are
listed in the table below.
-
The remaining 3 bytes hold the length of the packet
data, not counting the header. So for RPC codes
with no data, the 24-bit length value will be zero.
The length is sent for convenience to client writers.
It allows you to skip over packets that you don't know how
to handle, and it also makes decoding the data more convenient.
The following table lists the RPC codes currently in use
by the client/server protocol. Each code is the first byte
you receive in a request from the server. It tells you what
function to invoke on the client. The byte is usually
followed by some data - the arguments to the request.
Wherever we say "utf" below, it means "2 bytes specifying
the length (in bytes) of the following string, which is
utf8-encoded."
RPC Code |
Name |
Meaning |
Data To Read |
2 |
CONNECT_RESULT |
handshaking packet |
- int: major and minor protocol version numbers
(VERSION_MAJOR << 16 | VERSION_MINOR)
- byte: LOGIN_SUCCESS (3) or LOGIN_FAILURE (4)
- if failure, then:
- byte: text style
- utf8 error message
|
8 |
SEND_TILE_MAPPINGS |
server is sending over the current mappings of tile numbers
to image paths.
|
- int: compressed data length
- int: uncompressed data length
- byte[]: the compressed data, consisting of
alternating short values (tile numbers) and UTF8
strings (2 bytes length, n bytes utf8-encoded data).
|
11 |
TEXT_OUT |
server is sending some text output |
|
12 |
PLAY_MUSIC |
client should play a music file |
utf: filename (relative path under "music/" dir) |
13 |
QUIT |
closes the connection |
none |
14 |
SERVER_TRANSFER |
directs client to connect to a different host |
utf: the hostname to reconnect to |
16 |
SEND_TILE |
server's sending a tile number and the image it's mapped to |
- short: the tile number
- utf: the path to the image under the art/ directory
- long: the timestamp
|
17 |
SEND_IMAGE |
server's sending an image |
- utf: where to save the image
- int: image data length
- byte[]: the image data
- long: image timestamp
|
18 |
UNCACHE_IMAGE |
tells the client to discard a cached image |
- utf: the path to the image under the art/ directory
|
19 |
SEND_PICTURE |
sending a picture to display in a text window |
- int: image data length
- byte[]: image data
- utf: image title, to use if image can't be displayed
- byte: image format (0=GIF, 1=PNG, 2=JPG)
- byte: image alignment (0=left, 1=center, 2=right)
- byte: view in which to display the image:
- 0 = normal server text-output window
- 1 = chat window (currently there's only one)
- 2 = popup window
- byte: image flags (currently unused)
|
24 |
ZIP_SCREEN |
sends a full redraw of the map |
- int: (terrain array width << 16) | terrain array height
- int: zipped data length
- int: unzipped data length
- byte[]: compressed data, described below
|
51 |
ZIP_TEXT_OUT |
sending enough text data that it needed to be gzipped |
- byte: text style
- int: zipped data length
- int: unzipped data length
- byte[]: gzipped utf-8 data
|
52 |
ZIP_PARTIAL_SCREEN |
sending a partial screen update |
- int: zipped data length
- int: unzipped data length
- byte[]: gzipped screen data, described below
|
60 |
EXTENDED_COMMAND |
a placeholder |
none yet |
70 |
INV_ADD |
adding an item to inventory |
- utf: name (the name of the item)
- utf: commands (a whitespace-delimited list of command verbs
valid for this item, e.g. to be shown in a popup menu)
- short: the image tile number
- short: the index in the inventory to add the item
- short: the (x, y) drawing offset, for larger images.
Both X and Y are "tile offsets", meaning multiples of
32 pixesls. The high byte is X, and the low byte is Y.
If (y > 127), you have to subtract 256 from it to
convert to an unsigned value.
|
71 |
INV_REMOVE |
removing an item from inventory |
short: the index of the item to remove |
72 |
INV_MODIFY |
modifying an item in inventory |
- utf: the new item name
- utf: the new item commands
- short: the tile number
- short: the index of the item in inventory
- short: the (x, y) drawing offset (same as for INV_ADD)
|
73 |
INV_RESEND |
resends the entire inventory. |
- int: the zipped data length
- int: the unzipped data length
- byte[]: the zipped data, described
below.
|
80 |
GND_ADD |
adding an item to the ground view |
identical to INV_ADD |
81 |
GND_REMOVE |
removing an item from the ground view |
short: the index of the item to remove |
82 |
GND_MODIFY |
modifying an item in the ground view |
same as for INV_MODIFY |
83 |
GND_RESEND |
resending the entire ground view |
same as for INV_RESEND |
100 |
STAT_SET_HP |
resending hit points |
int: new HP |
101 |
STAT_SET_SP |
resending spell points |
int: new SP |
102 |
STAT_SET_XP |
resending experience points |
int: new XP |
103 |
STAT_SET_FOOD |
resending food level |
int: new food level |
104 |
STAT_SET_GOLD |
resending gold amount |
int: new gold amount |
105 |
STAT_SET_LEVEL |
resending player level |
- byte: new level
- int: xp required for next level
|
106 |
STAT_SET_NAME |
resending player name/title |
utf: new name/title |
107 |
STAT_SET_RANGE |
resending readied range weapon or spell |
- short: tile number for readied item
- utf: readied item text
|
108 |
STAT_SET_LOAD |
resending inventory load |
- int: current inv weight, in grams
- int: weight to be encumbered, in grams
- int: weight to be burdened, in grams
- int: weight to be strained, in grams
- int: weight to be immobilized, in grams
- utf: name of the current load, e.g. "encumbered"
|
109 |
STAT_START_POISON |
player is poisoned |
none |
110 |
STAT_STOP_POISON |
player is no longer poisoned |
none |
111 |
STAT_SEND_SPELLS |
sending spells the player knows |
utf: a whitespace-delimited list of spells. Spell names
have underscores in them instead of spaces, e.g. "word_of_recall" |
120 |
STAT_UPDATE_ALL |
updates a bunch of stats at once |
- int: hp
- int: max hp
- int: sp
- int: max sp
- int: level
- int: xp
- int: xp for next level
- int: food
- int: max food
|
The data from a ZIP_SCREEN request is a bit complicated.
The server sends two data structures:
-
a 2D terrain array of ints, consisting of information for
drawing the terrain and borders. The array is
2 tiles taller and wider than the map view, since
the terrain just outside the view is used in
the terrain-bordering algorithm.
-
a 1D array of object data, consisting of pairs
of 16-bit signed short values. The first short holds
the (x,y) coordinate at which to draw the object.
The second short holds the object's tile number.
For any objects that have alpha transparency,
there will be a third short value that holds
the alpha level (0-100). You can tell if there's
an alpha value for a tile if it has the high bit
set (0x8000). This value is described further
in the Alpha Compositing section.
The data structures are written to the stream as follows:
- short: terrain array width (2 tiles wider than map view)
- short: terrain array height (2 tiles taller than map view)
- int: compressed data length
- int: uncompressed data length
- byte[]: a compressed buffer holding the terrain and object arrays
To get the data out of the buffer, first read the terrain
array. Here's the java code that reads the data:
// byte[] unzipped = (the uncompressed data buffer)
// width = terrain array width
// height = terrain array height
int[][] terrain = new int[width][height];
int count = 0;
for ( int i = 0; i < width; i++ ) {
for ( int j = 0; j < height; j++ ) {
int b1 = unzipped[count++];
int b2 = unzipped[count++];
int b3 = unzipped[count++];
int b4 = unzipped[count++];
if ( b1 < 0 ) b1 += 256;
if ( b2 < 0 ) b2 += 256;
if ( b3 < 0 ) b3 += 256;
if ( b4 < 0 ) b4 += 256;
int t = (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
terrain[i][j] = t;
}
}
The way to interpret the terrain data is described below,
in the Terrain Borders section.
Next, read the object data. Again, the code might be
the easiest way to see how to do it:
/**
* Unzips the incoming data an performs a drawTerrain()
* followed by drawObjects().
*
* @param width the width of the terrain array
* @param height the height of the terrain array
* @param len the length of the unzipped byte array
* @param data a gzipped array of bytes representing the
* int[][] for the terrain and the short[] for the objects.
*
* @author stevey
*/
public void drawZippedScreen ( int width, int height,
int len, byte[] data )
throws IOException
{
byte[] unzipped = Strings.uncompress ( data );
// unpack the data
int[][] terrain = new int[width][height];
int count = 0;
for ( int i = 0; i < width; i++ ) {
for ( int j = 0; j < height; j++ ) {
int b1 = unzipped[count++];
int b2 = unzipped[count++];
int b3 = unzipped[count++];
int b4 = unzipped[count++];
if ( b1 < 0 ) b1 += 256;
if ( b2 < 0 ) b2 += 256;
if ( b3 < 0 ) b3 += 256;
if ( b4 < 0 ) b4 += 256;
int t = (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
terrain[i][j] = t;
}
}
// unpack the remaining bytes into a short array
int size = 0;
int num = (len - count)/2;
short[] objects = new short [ num ];
while ( count < len )
{
int lo = unzipped[count++];
int hi = unzipped[count++];
if ( hi < 0 ) hi += 256; // convert back to unsigned
if ( lo < 0 ) lo += 256;
objects[size++] = (short) ((hi << 8) | lo);
}
// draw the terrain, then the objects
display_.drawAllTerrain ( terrain );
display_.drawObjects ( objects );
display_.flushGraphics();
} // end of drawZippedScreen
Here's the code from the map display class that interprets the
terrain and data arrays:
/**
* Draws all the terrain in a specified rectangle.
*
* @param terrain an array of int-sized records containing enough
* information to draw the terrain and borders. The array is larger
* than the view by 2 in each dimension, and the view is centered
* in the array. (That is, there's a 1-tile ring all the way around
* the view of tiles you can't actually see in the view; they're
* used to compute the borders for terrain at the view edges).
*
* @see wyvern.common.util.RemoteMap#drawAllTerrain()
* @author stevey
*/
public void drawAllTerrain ( int[][] terrain )
{
int width = terrain.length;
if ( width == 0 ) return;
int height = terrain[0].length;
if ( height == 0 ) return;
// draw the terrain in first.
for ( int j = 1; j < height-1; j++ ) {
for ( int i = 1; i < width-1; i++ ) {
// mask out high word
int tile = terrain[i][j] & 0xFFFF;
drawTile ( i-1, j-1, tile, 0 );
}
}
// draw borders in visible portion of the view
if ( drawBorders_ ) {
drawBorders ( terrain, 0, 0, 0, 0, tileWidth_, tileHeight_ );
}
} // end of drawAllTerrain
/**
* Draws a sparse layer into the map.
*
* @param layer an array of alternating "stuffed" (x,y) view locations
* and tile numbers for those locations. If the high bit is set
* on the tile number, the following short is extended info for
* that tile.
* @author stevey
*/
public void drawObjects ( short[] layer )
{
if ( layer == null ) return;
int i = 0;
while ( i < layer.length )
{
// extract the high & low bytes into their own word values.
// these are signed byte values from -128 to 127, so
// we have to specifically treat them as unsigned. The
// ">>" operator preserves the sign for X, so we only
// need to do the subtraction for y. (Why are negative
// values allowed? Because the object's upper-left corner
// might be to the left of the view, or above it).
short temp = layer [i++];
int y = (temp & 0xff);
if ( y > 127 ) y -= 256;
int x = (temp >> 8);
short tile = layer[i++];
short alpha = 0;
// check 16th bit set
if ( (tile & 0x8000) != 0 ) {
alpha = layer[i++];
tile &= 0x7FFF; // clear it to get real tile number
}
drawTile ( x, y, tile, alpha );
}
} // end of drawObjects
You may prefer to create Objects or structs to encapsulate each
object while you're unpacking the data stream, for holding the x, y,
alpha, and tile number.
Partial-screen updates tell the client that only part of the map
needs to be redrawn. They're generally smaller (usually much smaller)
than full-screen updates. They necessarily include the terrain info
(including border info for neighboring squares) for any areas that
need to be redrawn.
A partial-screen update logically consists of a list of squares
to redraw in the map view. The squares don't have to be adjacent
to each other: you could have, for example, the upper-left and
lower-right squares redrawn as part of one partial-screen update.
The data comes as a compressed byte array. When uncompressed,
it contains 3 sub-arrays. Each sub-array starts with an int for the
length, followed by the data.
The three sub-arrays are:
- squares to redraw
- terrain info for those squares
- non-terrain objects for those squares
The first sub-array, the locations to redraw, is a list of short
values. It starts with an int (the number of short values),
followed by that many short values. The short values are
"stuffed" (x, y) pairs, in view coordinates. The java code to
unpack them looks like this:
short temp = locs[i];
int y = ( temp & 0xff );
if ( y > 127 ) y -= 256;
int x = ( temp >> 8 );
When unpacked like this, the X and Y values are coordinates
ranging from 0 to the size of the map view (minus one).
The second sub-array is the terrain data. It's a 3-dimensional
array. The top-level dimension is the same length as the
locations array. Each element of the top-level array is a
2d int array of terrain records used to compute terrain borders.
Each 2d int array is a 3x3 array of integer "terrain records",
described below. The center record,
at location [1][1] in the array, is the terrain at the location
being redrawn. The 8 neighboring ints are the terrain records
for the 8 squares bordering the location to redraw.
The third and final sub-array is another short array, just like
the first one. The values are tile numbers of the objects
to draw at each location specified in the first sub-array.
Wyvern currently uses a simple, effective terrain-bordering
algorithm designed and initially implemented by Frank Sronce,
a.k.a. the Archwizard Kiz. For any given square, the Wyvern
terrain-bordering algorithm determines the borders to draw by
looking at that square and the 8 neighboring squares.
Each square gets one integer bitfield, called a "terrain
record", that's laid out like so:
+-------------------------------+
|1|1|1| 13 bits | 16 bits |
+-------------------------------+
/ | |
border layer tile
flags priority number
A brief description of the fields follows:
- The low 16 bits are the terrain tile number.
- Bits 17-28 are the terrain priority, a signed number.
(Bit 28 is the sign bit).
- Bit 29 is the "has-borders" flag; true if the terrain has borders.
- Bit 30 is the "lets-in" flag: set if the terrain permits
borders to encroach into it, clear if the square disallows
incoming borders even from higher-priority squares.
- Bit 31 is the "lets-out" flag: set if the square wants to
extend borders (only meaningful if the terrain type actually has
borders), clear if the square doesn't want to use any borders it might
normally have.
The code that actually implements the terrain-bordering algorithm
is tricky enough that we've provided the java source code used
in the game (in all the Java clients, and in the Map Editor).
You can view it here:
Borderer.java
It's certainly not the prettiest (or fastest) code in the world,
but it works. It is one of the slowest pieces of the
map-drawing code on the client, and we see significant speedups
in the Java client when we turn off terrain bordering - an option
available in one of the menus.
We obviously need to provide a great deal more information
about the algorithm and the shapes of the 20 border tiles.
We don't expect client writers to reverse-engineer it.
Until this documentation is completed, however, you can draw
your maps without terrain borders, as follows:
- for each 3x3 terrain array, discard all but the center
(1, 1) int value.
- for the center value, mask out the high 16 bits, and
the result is the tile number of the terrain to draw
at that square.
We'll complete this documentation as soon as one of our several
prospective client writers is ready to make use of it.
When the server sends text (to the output window, but not to
inventory or anywhere else), it includes a style byte so
you know what color and font to use for it on the client.
The style codes are defined as follows:
code name |
value |
meaning |
TEXT_PLAIN |
0 |
normal text |
TEXT_INFO |
1 |
signs and books |
TEXT_ECHO |
2 |
echoing player commands back to client |
TEXT_DAMAGE |
3 |
player is taking damage |
TEXT_TALK |
4 |
someone has said something out loud |
TEXT_SYSTEM |
5 |
messages from the system,
such as "Please wait while this map is loaded."
|
TEXT_HIT |
6 |
player did damage to an opponent |
TEXT_WIZ |
7 |
wizard chat channel |
TEXT_TELL |
8 |
style for player-tells |
The INV_RESEND and GND_RESEND calls have identical data, except
that they're targeted at different views.
The packet consists of a zipped byte array, which has the
following standard format:
- an int containing the length of the zipped data to follow
- an int containing the length of the data when it's unzipped
- the zipped data
When unzipped, the data buffer contains four byte arrays.
The first two are string arrays, laid out as follows:
- an int: the number of bytes in the following string array
- an int: contains the number of strings in the byte array
- that many utf8 strings (each one 2 length bytes followed
by the utf8-encoded data, as usual)
The second two are arrays of short (16-bit) values, laid
out like so:
- an int: contains the number of short values to read
- that many short values
The four arrays all the same length. Each index has the
data for one inventory item. For instance, index[0] of each
of the 4 arrays has some data for the first inventory item.
The arrays are defined as follows:
- the item names
- the commands for the items. Each string is a
whitespace-delimited list of command verbs valid
for that item.
- the tile numbers for the items
- the (x, y) tile drawing offsets for the items.
Most of the time, these are zero, since most items
in the game are one "tile" (32x32 pixels).
Wyvern currently supports a limited form of alpha compositing
(in other words, partial transparency). It's supported at the
image level, not at the pixel level.
Alpha and transparency are complex topics, and it's beyond the
scope of this document to cover them. If you're unfamiliar
with them, there are many tutorials on them on the web.
Wyvern actually supports two forms of specifying transparency:
- Per-pixel full transparency. This is built into the
GIF (and PNG) image file format. When you create an indexed
GIF or PNG image in an image editor (such as Photoshop),
you can specify that one of the 255 index colors is the
"transparent color". Whenever you use that color in the
image, then it should use the background color instead
when you draw the image to the screen.
This feature is used extensively in Wyvern to "fake" a
3-dimensional view. If you drop a shield, the area
in the 32x32 image around the shield is transparent,
and you'll see the ground there (or whatever else was
under the shield.)
- Per-image alpha blending. The partial-screen and
full-screen updates (as well as SEND_PICTURE) support sending
an alpha value along with an image. The alpha value is
described in this section.
Wyvern specifies alpha values as byte values ranging from 0 to 100.
If the value is zero or 100, it means the image has no alpha
component, and should be drawn fully opaque (i.e. drawn normally).
If the value is from 1 to 99, it means that the image should be
blended with the background using an Alpha Compositing algorithm.
Most programming frameworks provide support for alpha compositing
(also called alpha blending) in their graphics system.
In most systems, the alpha component ranges from 0.0 to 1.0,
where 0.0 is fully transparent and 1.0 is fully opaque. In
Wyvern, you simply divide the alpha value by 100.0 to get the
floating-point value from 0.0 to 1.0. You should first check
if the value is 0 or 100, and don't do alpha blending for
those cases.
Note that the PNG image format (which we'll migrate to at some
point) supports per-pixel alpha values, at 8 bits per pixel.
This requires 32-bit-per-pixel images, which are significantly
larger (up to 8 times larger) than Wyvern's indexed 256-color
images. We're not sure if this will result in a reasonable
experience for modem users, so the migration is on hold for now.
However, most images will probably NOT need transparency, so
we may be able to get away with only using 32-bit images when
it's required.
|