Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Tools

Creating Your Own Multiplayer Game Systems


SP 94: Creating Your Own Multiplayer Game Systems

Rahner and Linus are programmers in the Sacramento, California area. They can be reached on CompuServe at 71450,757, the Channel-D BBS at 916-722-1984, or voice at 916-722-1939.


Over the years, I've come to believe that one of the greatest challenges programmers can take on is the design of a computer-based game. When the scope of the game is limited only by the hardware and your imagination, there can be no higher peak than creating the ultimate multiplayer game.

In this article, I'm presenting the development tools you need to build your own complete, multiplayer game system. At the heart of this development platform is an "engine" for creating a variety of server-based games such as adventure games or space-type arcade games.

My focus here is on the design and features of the engine, not on details of specific game genres. I'll zero in on the game server (as well as its underlying database), the player nodes, and the terminal software that enables communication between the server and nodes. The complete game-development system is available electronically from DDJ (see "Availability," page 3) as well as from our Channel-D BBS.

Underlying Principles

Games require performance. While average computer users may wait patiently for dBase to sort a list of customers who have blue eyes and four warts, they start fuming when their 486/66 isn't fast enough to provide the windshield glare in "Aces of the Persian Gulf." Performance does require hardware, but more importantly, performance requires design and attention to those minor details that most programmers typically don't have to deal with.

Real-time games, whether arcade or incremental, have "turns." In a sequential game such as Monopoly, a turn is defined by the player upon completion of an arbitrary set of actions. Real-time games, however, define a turn by some external time constant, without regard to the desires of the participants; consequently, the performance of the computer is important. With arcade games, a turn is generally qualified by the time it takes to show the current state of the game (video vertical refresh, for example). Incremental games, on the other hand, are real-time games with an extended, fixed turn time--generally on the order of seconds. The extended time allows the computer to make more complex decisions and tends to equalize the differences in the physical reflexes of the participants.

One purpose of games is to provide the participants with a microcosm of life in compressed time. (Believe me, no one would want to play my life in real time; but, if it were provided in 15-minute bursts, maybe someone would stay awake.) It's been said that "the difference between a toy and a game is that a game has a goal and a toy does not; therefore life is a toy." In keeping with that concept, I will diverge from life and just refer to a reality. Life is absolute; reality is merely perception. We will create realities that can have goals; the life will be left up to the player.

Overall Structural Design

The game-development system presented here has three major logical structures: the server, nodes, and terminals. Each is logically independent of the others, but may reside within the same physical computer. There are direct communication paths from the server to the nodes and from the nodes to the terminals. For the sake of security, there is no direct path between nodes or from the terminals to the server.

The server is at the center of the entire system. Its purpose is to receive requests from the nodes, prioritize those requests according to some properties of the reality, and provide responses to the nodes in accordance with the state of the reality at the end of the turn. The server's purpose requires it to have complete control over an entire database; therefore, a majority of the storage resides within the server. The ability of the server to communicate with the nodes is essential, so consideration must be given to the message structure and the carrier to facilitate data transactions.

The node is either a gateway to the player's terminal or the source for actions of the nonhuman players that exist within the reality. As a gateway, the node receives request packets from the terminal, qualifies those requests, and possibly sends them to the server. The node provides all the security and secondary parsing services for the server. To this end, the connection between the node and server is assumed to be secure and the packets passed are incorruptible. As a source of nonhuman player actions, the node provides another reduction in server-CPU overhead. This node service does not necessarily eliminate the processing of nonhuman actions by the server.

The terminal lets the human player interact with the reality. Unlike most terminal programs, this one is graphics based, supports animation, and requires a fast CPU and significant disk storage. The node communicates with the terminal program with free-format packets that are similar to function calls made by the node to a terminal "process." Although the terminal program can reside within a node as a separate task, it more typically is a remote computer terminal linked through a modem. The packet structure is designed to ensure data integrity and assumes that the terminal has a large, local database that can be referenced. The general philosophy, with regard to the clarity of the physical connection between a remote terminal and the node, is that the game should not be crippled by the one bad connection. Volatile request packets are rejected with minimal retries. The terminal provides the initial syntax/range checking and parsing. Request packets are built and sent to the node. Because of the abilities and motivations of some gamers, the terminal is considered unsecure.

Inside the Server

When we thought about the ideal game server, two major components to consider were the hardware and the operating system. Although, collectively, we had experience with almost all microprocessors, microcontrollers, and RISC processors (and their necessary development tools), we chose the x86 platform.

Currently, the server is a 486/66-MHz with 32 Mbytes of RAM, a 1.2-gigabyte SCSI drive, a 5-gigabyte tape driver for historical information, and a WORM drive for terrain information. The three storage peripheral types are on separate bus masters to eliminate the data/command bandwidth problems we encountered when they were all on the same adapter.

Although there were several options in regard to the server's operating system, the one major concern was performance-- everything else was subordinate to raw speed. Among those OSs we considered were: NT, MS-DOS, RMX, and NetWare/ 386. Since processing performance requires RAM and MS-DOS doesn't let us access all of it unless we use a DOS extender, DOS hit the road. I have used RMX only long enough to decide that I don't like it, so it followed DOS. Pretty pictures and crippled CPU power wouldn't do, so NT was out. Ultimately, we chose NetWare/386.

As it turns out, NetWare has several advantages as a game server. It can handle a large amount of RAM. It has excellent communications, not to mention disk and tape support. It is non-preemptive, so the operating system will get out of the program's way when needed. It runs applications (NLMs) in Ring 0, so there isn't any instruction-virtualization nonsense. On the downside, debugging can be a problem and the user interface is poor.

We used Watcom C and Microsoft's MASM 6.0 assembly language package to build the system. Watcom C is great for generating NetWare NLMs, 286 executables for real-mode MS-DOS, and 386 protected-mode executables for DOS extenders. Value passing between functions was done using registers (as opposed to the stack) for the sake of performance. With the use of pragmas, specific passing registers are selected occasionally to the same extent.

General-Database Considerations

The database associated with the game server is large and dynamic. As the game is played, the database grows, and we had to plan for the database spanning hard disks and peripherals. As with most databases, there are static and dynamic portions. Rather than treating everything in some general manner, we chose access methods geared toward each case. The database can only be accessed through a single process on the game server, for instance, so multiple access concerns are virtually eliminated.

The hard-disk-file data is stored three ways: fixed-data files; hashed, binary-indexed files; and Btrieve. The access method for the database depends upon which delivers the best performance.

The fixed-data files are those that contain fixed data that can easily be accessed directly. These are generally configuration files that are read once and ignored during the operation of the program. Access time and complexity are not real issues in this case.

The hashed, binary-indexed files contain data in which the index table is infrequently altered, and no importance is placed upon the access order of data elements. This method is the fastest we could implement, so most of the databases use a hashed index. The index for this method is an array of 32-bit hash entries and associated file offsets in RAM or on disk. The hash is generated by performing a table-driven, 32-bit CRC on the index tokens for a record. The hash is then appropriately placed in the index array. When tested against a dictionary with 450,000 unique words, this hash method had only two collisions. The index search function (written in assembly) returns a worst case, noncollision matching entry in less than 8.0 microseconds when tested with a one-million-record index running on a 486/33. Since binary hashing does have some performance drawbacks when adding and deleting records, it wasn't universally applied.

Any database that did not fit within the previous two classes was indexed using Btrieve. We don't yet know whether this is the highest-performance index; when we compared several B-tree methods, no particular one emerged as the clear champ. The comparative performance of B-trees depends upon a variety of factors such as the content and size of the data, the order that data is entered, how thrashed the disk is, and the phase of the moon. The arbitrary nature of the data that will be filed by this program and the NetWare-specific target led us to Btrieve. If you know of a demonstrably better index than Btrieve, we would be very interested in hearing from you.

Server Communications

In a network game, there are two communication philosophies: client/server or web. Initially, we considered a network web, where every node is a peer, sharing the overall processing load. However, this approach quickly turned into a headache. The requirement for intercommunication in the web is considerable. In certain, easily attainable scenarios, the horrendous network traffic caused lost packets, which in turn, increased the network traffic for retransmissions. In a web, there is a major synchronization problem. Additional communication packets are required to make sure every node sticks to a common time scale. Also, with increased complexity, comes an increased vulnerability to failure. One computer out of one is less likely to go down than one out of many. A client/server arrangement tends to be self-limiting. With this in mind, we ultimately settled on the client/server model.

With client/server architectures, communication between nodes and the server can be a major bottleneck. Currently, network communication is via IPX packets in a raw 802.3 format on 10-MHz Ethernet. This yields about 500 Kbytes/sec gross throughput--adequate for a low-volume system. We'll likely switch to 100-MHz Ethernet as it becomes stable.

From an installation standpoint, we've eliminated two more layers by not using LSL. We found a slight performance improvement over LSL/ODI by using the old, bound IPX.COM driver. Of course, this performance difference may be a function of the network-interface manufacturer we use. We have not done extensive multivendor testing to see if this performance enhancement generally holds.

To facilitate the initial access to the server, we have followed Novell's Service Advertising Protocol (SAP). A game server appears on the network as a type 0x8900 server. Beyond making a call to NetWare's SAP function, no further consideration was given to this portion of the server. The communication socket is whatever NetWare decides to dynamically allocate for our process. The remote node pings for the server on startup using SAP. SAP delivers the logical network address and socket number for the node to start a connection. Except for the initial SAP query, no broadcast packets are used.

The data-transaction protocol we chose is similar to Novell's own NetWare Core Protocol (NCP). Novell does provide access to its NCP layer, but we decided that we would need something a little more loosely coupled, and we were less concerned about network security at this level. By using our own communication construct, we are not constrained by the connection limit imposed on the five-user versions of NetWare.

All transactions are initiated by the remote node. The node either requests a response or transmits a connection-good packet. The requests are processed by the server and responses are returned upon completion of their execution within the server. Simple status requests are returned within a millisecond; turn requests are returned on completion of the next game turn (50 milliseconds to 5 seconds). Because of the extended time required for turn resolution, the server sends an immediate acknowledgment packet for turn requests. This allows the node to maintain a tight time-out on turn requests and resend that packet if the need arises. Even so, the game running on the server must contend with turn packets not being submitted due to lost packets. The basic request-response packet is shown in Table 1.

Although a majority of the request packets have only one request, the transmission packet may contain multiple requests in the form of "tuples" (an AppleTalk term that I've shamelessly plagiarized). The length field of the IPX header is used to determine the number of tuples (that is, to read through each of the tuples until the length gives out). The format of a single tuple is listed in Table 2.

The request tuples made by the node are numbered sequentially, and the server's responses are given the same number. The server may answer all requests from a node with separate response packets or may respond in kind with a multiple tuple packet. The server does not keep track of the sequence order; that is the responsibility of the requesting node. If a response has not been received by the node for some request within the appropriate time, the node can either resend the command or send a packet-status query request. If the node resends the command, either an identical or new sequence number can be used. If the same sequence number is used, the packet-resend bit must be set to inform the server not to process the packet twice if the original request is sitting in some process queue. If the packet-resend bit is not set and the original packet has not been processed, the server will treat the new packet as a replacement for the old, which is then discarded. If a new sequence number is used, the node must be careful that those requests do not have a detrimental effect if both requests were processed.

The first request made by a node to the server is for a valid connection number. This is accomplished by sending a packet to the server with the connection number field set to 0xFFFF and bit 5 of the flags field set to 1. Any tuples encapsulated in this packet will be ignored by the server. The node must set the destination-network address and socket in the IPX header according to the information found in the server SAP packet. The source socket must be set to the socket number to which the node will be listening for responses from the server. If the node's listening address and socket number are the same as an existing connection, the previous connection will be closed in the server and a new connection entry will be generated. If multiple processes are accessing the server from the same node, they must all use socket numbers that are distinct from the others being used on that physical node. An establish-connection request does not require any sequence number. The server will respond with a valid connection number (0-0xFFFD) and bit 5 of the flags field set to 1. The node may send this request repeatedly until it receives a good response. If bit 3 of the flags field is set in the response, the server does not have room for an additional connection.

When the initial connection is established, requests may be made of the server in any order. The server performs a check of all of its active connections every five minutes. If any connection has not made a request of the server within that five-minute period, that connection is closed. It is up to the node to make simple time requests every minute just to guarantee that the server does not close the connection.

If a node makes a request using connection number 0xFFFE and bit 5 of the flags field set, the server will return the connection number currently being used for that node ID/socket combination. This transaction was implemented to reduce the damage done if the node fails/reboots/dies, but is able to recover its composure enough to carry on the connection. Nothing's worse than being involved in a real-time game when a machine crashes and the player has to start over--except when everyone has to start over. This transaction should not be used routinely (preferably only once during the life of a connection), because it takes more server CPU cycles than a time request or a connection-status request.

Server Requests

Requests by a node can be directed at the server or a game being played on the server. Each request type is differentiated numerically by the first byte of the request body. Each request type has a subtype byte that immediately follows the first. The request types are listed in Table 3.

A request-type number with bit 7 set is intended for a specific game. The lower seven bits are used for a game handle to determine for which game the request has been made. The packet is sent to that game function for processing. Each game has its own request structures.

No visuals are associated with the request/response transactions. The game server is only aware of spatial relationships. The time and processing required to generate graphics and interact with the user are unnecessary for the operation of the game server. The data required for visual and audio can easily overwhelm the bandwidth of any communication channel; therefore, those two components have been left out of game-server requests.

The Node

The application (called "NODE") running on the node would generally be started from some BBS as a DOOR (an executable that expects to get its user interaction from a serial port). As a DOOR, we had a choice of accessing the serial port through a FOSSIL driver or directly to the serial port. We chose the direct-access method, more for the fact that the FOSSIL did not provide us with certain capabilities rather than any performance issue.

NODE and the terminal program require a certain amount of multitasking, or at least a close approximation. Our target for the node and terminal was a 386-based DOS PC primarily because there are a lot of them out there and we could buy them cheap. Mixing multitasking and DOS can be done in several ways. While we looked at DesqView (too slow), AMX (too expensive for little gain), and others, we eventually wrote our own polled-event manager that works fine. The major operational parts of the event manager are written in assembly language. Drivers were written for all the communication events that need to be processed--network, serial I/O, keyboard, and timer. NODE's main() simply initializes the screen and the event manager; then it calls the event-polling function, which loops continually, accepting and processing messages from the event manager. The event poll does not return to main() until the program is ready to return to DOS.

Upon initialization, NODE attempts to establish a connection with the remote computer in order to assure that the program is running a current version of the terminal program (named TERM). If the remote is not running TERM, a simple text message is sent telling the user to download it, then NODE exits to DOS. If TERM is running on the remote but is not a current version or is missing resource files, NODE triggers a download of the current environment. Once everything is found to be valid, NODE begins its transaction process with TERM.

To establish a connection, the following steps are taken:

  1. NODE begins transmission of a BREAK sequence.
  2. TERM begins transmission of a responding BREAK sequence within 250 milliseconds.
  3. NODE stops the BREAKing and transmits the message "RYUNODExxxxx". The first seven bytes (RYUNODE) are the signature and are mandatory, but any other data may follow terminated by a 0. The full signature string must be received within one second of the end of the BREAK.
  4. TERM stops BREAKing when it receives the first non-BREAK character and transmits its message "RYUTERMxxxxx" in response. The first seven bytes are the signature anything may follow that, terminated with a 0. The full response string must be received within one second of the end of the BREAK.
  5. NODE sends the first command for TERM's version and resource status.
  6. NODE starts any file transfers that are required.
The BREAK signal was chosen because it exists outside of the 8-bit data set that generates its own interrupt. By having a signal outside, we can resynch a command-based communication interlock without resorting to artificial data sequences that can be easily lost in a burst of noise. The BREAK resynching procedure (steps #1--4) can be used whenever either side finds that it has lost command track.

Where the server is the id, NODE is the ego of the game. It determines how the user will communicate with the server. It decides what will be displayed on the terminal. It handles any security processing, and it performs communications with the other nodes.

As mentioned earlier, NODE communicates with the server using request/response transactions. That same application communicates with the user's terminal using command/acknowledgment transactions. The request/response transaction is a loose interaction, where the volatility of the data makes the importance of lost transactions moot after the turn boundary; therefore, a missing communiqué is generally shrugged off. Conversely, the command/acknowledgment transaction views lost packets as a reason for suicide, or at least motivation for extreme mid-life crisis. The direct link between NODE and TERM complements the command/acknowledgment because there are no other connections to usurp control of that data conduit as there are with Ethernet. Additionally, the cumulative nature of the visuals and the fact that memory buffer references are passed between NODE and TERM require a qualified data handshake.

The structure of the command sent by NODE is shown in Table 4. The commands are sent from NODE to TERM in any sequence order. A sequence number will be accepted only if there is no previous command that contains that sequence number. A command does not require any acknowledgment before the next command is sent, but each command must be acknowledged within 50 milliseconds of the transmission of the last byte of that command. A command can be acknowledged in three ways: ACK with status, ACK with an indefinite delay, or a command resend.

An acknowledgment with status has the structure described in Table 5. If the status byte is 0xFF, the status body will contain a time-out (in system ticks), during which NODE waits for the actual acknowledgment. Statuses from 1 to 254 can be used to indicate any condition other than complete success in fulfilling the command request.

An acknowledgment with indefinite delay would have the structure listed in Table 6. This would tell NODE to wait forever for the command to complete. Needless to say, if too many of these ACKs are pending, the connection will require a resynchronization.

Because each command/ACK packet starts with a length byte, this length will only contain values from 3 to 255. This leaves 0, 1, and 2 for special circumstances.

  • 0 is a common side effect of the BREAK sequence, so it is ignored by both sides.
  • 1 can be sent by either side to force the other to resend all unacknowledged packets.
  • 2 has been reserved for future use and is currently ignored.
If the CRC shows that any packet is incorrect, the receiver sends a resend (1) request, after any current transmission is completed. This requires the ISR on the receiver to calculate the CRC as the packet bytes are received. Because packet length precedes any data, the entire packet does not have to trigger an event until it has been completely received and qualified. The ISR is also responsible for keeping track of any ACK time-outs.

NODE/TERM Commands

Both NODE and TERM are event-based processes. After they do their respective initializations, they jump to the event manager; returning only to exit to the operating system. Commands sent to TERM by NODE are fashioned after a C function call. By convention, all remote function calls are differentiated by an RM_ prefix. These "functions" are actually macros that pass data to the serial-communication routines before releasing to the event manager. The event manager attaches some task values to that remote call and continues polling the event queue for event packets to process. This simple, no-frills multitasking system works well when there is an unknown amount of time between the command and its response.

The major commands are shown in Table 7. The minor-command numbers depend upon the major command. The command takes one byte. The subcommand number is also one byte. No command is processed by NODE until it has been fully received and qualified. Once that command has been completed by TERM, a completion response is returned to NODE.

For example, assume you want to move the graphics cursor to the x,y pixel position 100,230. The program in NODE would have the command line rv=RM_gotoxy( 100, 230 );. This, in turn, would be expanded by the C compiler to rv=send_command( CMD_GRAPHICS, RM_GOTOXY, 100, 230 );. The send_command() function would package and transmit the information in Figure 1 to TERM.

All remote-memory references are done with16-bit handles. This allows TERM to use any form of local-memory management. Actual data transfers between NODE and TERM are rare, because it is expected that TERM has a complete database of images, icons, and fonts. If an image does not currently exist in TERM's local database, a general group image is used as a placeholder until an image-request command from TERM is serviced by NODE. Although the command/acknowledgment transaction can go both ways, NODE does not support any commands except status and query for security reasons.

Implementation

Both NODE and TERM are written with Watcom C and MASM 6.0 assembler. Both programs are run in protected mode using Rational System's royalty-free DOS4GW DOS extender. One of the pluses of this environment is that it has tons of memory. The extender runs on most 386 (and up) machines without a hitch. It almost supports the DPMI 0.9 specification and has a flat memory model that directly maps the value of a pointer to any physical address--no segment-register diddling.

On the downside, the environment doesn't fully support the DPMI 0.9 specification--real-mode callbacks to protected-mode functions are not supported (DOS4GW supports only the mouse callback). Interrupt-service routines (ISRs) written to be serviced in protected mode run as slow as molasses. Certain CPU instructions trigger CPU faults, which cause a fault manager to be run; this can make STI and CLI instructions take several microseconds to execute.

Whenever an interrupt occurs while running DOS4GW, the CPU is switched to real-mode, and the real-mode ISR is run first. After the real-mode ISR has returned (even if it was just an IRET), the CPU switches to protected mode to process any protected-mode ISR. The CPU then switches back to whatever mode was running at the moment of the interrupt. This entire process takes a bit of time. Our solution was to implement device drivers as .COM programs and dynamically load them into low memory from files at run time. This required placing the event manager's memory and certain input functions in low memory as well. By putting the drivers in low memory (and making them real-mode drivers), we solved other problems beyond the slow ISR. Because the drivers and event manager are the major users of privileged instructions (STI, CLI, OUT, IN), being run in real mode means that those instructions do not generate a fault. Also, because event-manager input functions are written in real mode, the mouse and IPX event vectors do not require a real-to-protected-mode callback. The real-mode drivers support the keyboard, timer, serial ports, mouse, sound I/O, and network interface. I suppose there could be others, but what else that generates an interrupt is useful for a game?

The implementation of each of the viewing styles presented to the user is beyond the scope of this article. Whether the user gets a straight overhead view, a tilted overhead view, or a straight-on three-dimensional, Doom-style view is immaterial to the actual inner workings of the game being played. In any event, the display template has been implemented and the internal, relational data structures are the same.

The implementation of the low-level graphics engine is important. A large portion of TERM's CPU time is spent updating the screen and building the visuals. Some of the time is apparent to the casual observer, all those images require the movement of a lot of data. Other demands on CPU time are more obscure. Compared to normal RAM, accessing video memory is like running in loose sand. One reason is that video memory is on the bus and CPU caches do not deal with that memory. Another reason is that dual-ported, video RAM (VRAM) will delay a second write to video memory until the previous write has been completed. We came up with a couple of solutions to reduce the effect felt by the slow video. Any references made here to video assume a 16-bit VGA adapter in Mode 13 or Mode X.

The delay of the CPU by the VRAM between successive writes is significant. To find out how much, we wrote the test functions in Example 1. All of these programs executed in the same amount of time. What this shows us is that we can reduce the amount of time it takes to perform graphics functions in several ways. First, most video accesses are repetitive manipulations of the same areas. By creating a virtual video buffer in RAM to redraw the screen at every video refresh (or timer tick), we reduced the delays associated with continually accessing the same areas; see Listing One, page 63. We also kept track of the region in which the updates were made, so only the general regions that had been updated since the previous video refresh were written. Each change enhanced the system's performance, but we still had not dealt with the time wasted between video accesses. We created a jump vector to be called after every video access. A jump vector would allow small program snippets to run, getting some use out of the CPU while the video came back on line. We created a second virtual buffer, which represented the current video state. Statistically, many of the colors did not change in regions, even though the images were different. By comparing the update regions between the two virtual buffers, we eliminated many unnecessary accesses. Performing only 16-bit writes gained additional time slots to do comparisons. Cycles are still being lost, but the comparisons tend to take up a bit of that slack.

Conclusion

As you might guess, this project is an ongoing effort. Anyone can take part; your ideas, suggestions for changes, and help are welcome. To participate, dial up the Channel-D BBS at 916-722-1984, 916-722-1985, and 916-722-7223.

Table 1: Basic request response packet.

Packet   Name                Description

00--29   IPX header          Information associated with the IPX packet.
00--31   Connection number   Number used to define the client/server 
                              connection.
30--31   Packet flags        Bits used to define the packet that follows.
                             B0: 1 indicates that the packet is a heartbeat
                              or an ACK.
                             B1: 1 indicates that this is a resent packet.
                             B2: 1 indicates that this packet must be ACKed
                              immediately.
                             B3: 1 indicates that the connection is being
                              terminated.
                             B4: 1 indicates that all enqueued packets
                              should be flushed.
                             B5: 1 indicates that the node is establishing a
                              connection.
                             B6: 1 indicates that the node is using an
                              invalid connection.
32--i    Request tuple 1    Encapsulated request 1.
i+1--j   Request tuple 2    Encapsulated request 2.
 ...
y+1--z  Request tuple n    Encapsulated request n.

Table 2: Format of a single tuple.

    Packet     Name              Description

    0--1       Request length    Length of request tuple to
                                 follow, including this length field.
    2--3       Sequence number   Number used to differentiate requests
                                 (0--2047 valid).
    4--n       Request body      Body of the request.

Table 3: Server request types.

Type     Name

00     Status or General Information
Covers the status or information for the server, any connection, a player, or a game.

01     Control
Allows the node to establish a player and attach to or drop from a game.

02     Communicate
Allows a connection to communicate with another player or connection. This is used to send or receive disk- or server-based e-mail.  If direct communication is desired, each receptive node will have listening sockets open from the socket with which they use to communicate to the server. That socket information is gleaned from a status request.

03     Query an Entity
Allows the connection to get information on an entity in the main database. An entity is defined as an existing player or nonplayer thing or object that currently exists within a reality. This can be a person, place, or thing. The entity must exist and have a name or a handle.

04     Query a Species
Allows the connection to get information on a species that exists within the main database. A species is defined as a template from which entities are created. Species can be animal, vegetable, or mineral.

Table 4: Structure of the command sent by NODE.

Command      Name              Description

0            Command length    Length of command from 1 to n+2.
1            Sequence number   Number used to differentiate commands,
                                0--127.
2 to n       Command body      Body of the command.
n+1 to n+2   CRC               16-bit CRC of bytes 0 through n, inclusive.

Table 5: Structure of acknowledgment with status.

Status       Name              Description

0            ACK length        Length of packet to follow, from 1 to n+2.
1            Sequence number   Command # ACKing with bit 7 set, 128--255.
2            Status byte       Status for the command, 0 is always good.
3 to n       Status body       Additional status bytes that are command
                                dependent.
n+1 to n+2   CRC               16-bit CRC of bytes 0 through n, inclusive.

Table 6: Structure of acknowledgment with indefinite delay.

Byte Length   Name              Description

0             ACK length        Length of packet to follow, 3.
1             Sequence number   Command # ACKing with bit 7 set, 128--255.
2 to 3        CRC               16-bit CRC of bytes 0 and 1.

Table 7: NODE/TERM commands.

    Command   Description

      0       Status.
      1       Query.
      2       Watcom C library access.
      3       Memory management.
      4       Hardware access.
      5       Graphics primitives.
      6       Menuing access.
      7       Window access.
      8       Animation.
      9       Data manipulation.
     10       Database manipulation.
     11       Messages (mail, chat, and sound).
    254       Continuation of previously sent packet.
    255       Extended major command, byte that follows is extended-command
               number.

Example 1: Three test functions to determine CPU delay.

(a)
           mov   ecx, 32000
           mov   edi, 0A0000h
           xor   eax, eax
           rep   stosw

(b)
           mov   ecx, 16000
           mov   edi, 0A0000h
           xor   eax, eax
           rep   stosd

(c)
           mov     edx, 32000
           mov     edi, 0A0000h
           xor     eax, eax
        loop10:
           stosw
           dec     edx
           jz      done
           mov     ecx, 20
        loop20:
           add     eax, eax
           loop    loop20
           jmp     loop10

Figure 1: Information transmitted by the send_command() function.

0     9      Command length
1     xx     Sequence number, managed by send_command()
2     5      (Graphics primitives)
3     11     (Function RM_GOTOXY)
4-5   100    X coordinate
6-7   230    Y coordinate
8-9   xxxx   16-bit CRC of bytes 0 through 7, inclusive

Listing One


; ****************************************************************************
; * Title:  GKERNEL.ASM
; * Copyright (c) March 1994, Ryu Consulting
; * Written by Rahner James
; * Code to move the virtual graphics buffer to the physical buffer
; * Very important note: These functions are designed for performance, so 
; *     very little (if any) range checking is performed on the data unless 
; *     the RANGE_CHECK label is defined
; ****************************************************************************

    .386
    .model  small, syscall

;RANGE_CHECK              equ 0    ; Defined if range checking is enabled
MAX_VIRTUAL_WIDTH         equ 320
MAX_VIRTUAL_HEIGHT        equ 200
VIDEO_START               equ 0A0000h
TIMER_INTERRUPT           equ 1CH

.DAta

Display_Width            dd  320
Display_Height           dd  200

Virtual_Display_Ptr      dd  0     ; -> virtual display buffer
Virtual_Display_Bytes    dd  0     ; Size of virtual display in bytes
Virtual_Display_Words    dd  0     ; Size of virtual display in 16-bit words
Virtual_Display_Dwords   dd  0     ; Size of virtual display in 32-bit words
Virtual_Display_End      dd  0     ; End of the virtual display
Lowest_X                 dd  0
Lowest_Y                 dd  0
Highest_X                dd  0
Highest_Y                dd  0

Video_Access_Count       dd  0     ; Number of video accesses since refresh
Update_Display_Flag      dd  0     ; Set to !0 if the virtual buffer should 
                                   ;      be moved to the display


Line_Start_Table         dd  MAX_VIRTUAL_HEIGHT dup(0) 
Old_Timer_Vector         dd  0,0 ; -> old timer ISR, !0 if installed

Old_Video_Mode           dd  0FFh

.COde
    extern  malloc_:near, free_:near, __GETDS:near

; ****************************************************************************
; * void MOVE_VIRTUAL_TO_DISPLAY( void )
; * Moves the virtual buffer to the display buffer
; * Given: Video_Access_Count = number of accesses made to the video
; * Returns: Virtual buffer copied to the display
; *  Lowest_X, Lowest_Y = lowest point in virtual display accessed
; *  Highest_X, Highest_Y = highest point in virtual display accessed
; *  Video_Access_Count = number of accesses made to the video
; *  Update_Display_Flag set to 0
; ****************************************************************************
move_virtual_to_display_ proc uses eax ebx ecx edx edi esi

    cmp       Video_Access_Count, 0           ; See if we need to do this
    jz        short done                      ; Quit if not

; * Setup the registers for the movement
    mov       esi, Lowest_Y                   ; ESI = lowest Y value
    mov       esi, Line_Start_Table[esi*4]    ; ESI -> virtual line start
    mov       eax, Lowest_X                   ; EAX = left X
    and       al, not 3
    add       esi, eax                        ; ESI -> upper left pixel in 
                                              ;                virtual buffer
    mov       edi, esi
    sub       edi, Virtual_Display_Ptr     ; EDI = offset from start of table
    add       edi, VIDEO_START             ; EDI -> upper left pixel in display

    mov       edx, Highest_X               ; EDX = right most side
    mov       ebx, Display_Width
    sub       edx, eax                     ; EDX = width in bytes - 1
    sub       ebx, edx                     ; EBX = display width remainder + 1
    dec       ebx
    and       bl, not 3                    ; Clear the LSBits

    add       edx, 4
    shr       edx, 2                       ; DL = width/4 (will work up to 1023
                                           ;                     pixels wide)
    mov       eax, Highest_Y
    sub       eax, Lowest_Y
    inc       eax

; * Loop through the rectangle, putting it down
@@:    mov       ecx, edx
       rep       movsd
       add       edi, ebx
       add       esi, ebx
       dec       eax
       jnz       @B

done:  mov       Lowest_X, -1
       mov       Lowest_Y, -1
       mov       Highest_X, 0
       mov       Highest_Y, 0

       mov       Video_Access_Count, 0   
       mov       Update_Display_Flag, 0      ; Stop any updates
       ret

move_virtual_to_display_ endp

; ****************************************************************************
; * void far TIMER_ISR( void )
; * Timer tick ISR
; * Given: nothing
; * Returns: 0 if all went well
; ****************************************************************************
timer_isr proc

    push       ds
    push       es
    pushad

    call       __GETDS

    inc        byte ptr ds:[0B009Eh]

    popad
    pop        es
    pop        ds

    iretd

timer_isr endp

; ****************************************************************************
; * int GR_INIT( int EAX )
; * Sets the graphics screen to a mode
; * Given: EAX = mode to set the graphics monitor to
; *         0 = normal EGA, mode 10h, 640x350, 16-color
; *         1 = normal VGA, mode 13h, 320x200, 256-color
; * Returns: 0 if all went well
; ****************************************************************************
gr_init_ proc near uses ebx ecx edx edi

    mov       Update_Display_Flag, 0      ; Stop any updates
    mov       Video_Access_Count, 0       
    mov       Lowest_X, -1
    mov       Lowest_Y, -1
    mov       Highest_X, 0
    mov       Highest_Y, 0

    mov       word ptr ds:[0B009Eh], 730h

; * Make sure we are using a fresh memory buffer
    cmp       Virtual_Display_Ptr, 0      ; See if already setup
    jz        short gr10_init             ; Skip if not

    push      eax
    xor       eax, eax
    xchg      eax, Virtual_Display_Ptr
    call      free_
    pop       eax

gr10_init:
; * Get the old video mode, if need be
    cmp       byte ptr Old_Video_Mode, 0FFh   ; been here before?
    jne       short gr20_init

    push      eax
    mov       ah, 0fh
    int 10h
    mov       byte ptr Old_Video_Mode, al
    pop       eax


gr20_init:
; * Revector the timer if we need to
    cmp      Old_Timer_Vector, 0            ; timer already installed?
    jnz      short gr30_init

    push     eax
    mov      eax, 204h                      ; EAX = DPMI Get Protected-mode
    mov      bl, TIMER_INTERRUPT            ;      Interrupt Vector command
    int      31h
    mov      Old_Timer_Vector, edx
    mov      Old_Timer_Vector+4, ecx
    mov      eax, 205h                      ; EAX = DPMI Set Protected-mode
    mov      bl, TIMER_INTERRUPT            ;      Interrupt Vector command 
    mov      cx, cs
    mov      edx, offset timer_isr
    int      31h
    pop      eax

gr30_init:
    cmp      eax, 1                        ; See if it's us
    jne      derr                          ; Quit with an error if not

; * VGA mode 13h, 320x200, 256 colors
; * Initialize all variables before memory allocation
    mov      Display_Width, 320
    mov      Display_Height, 200
    mov      Virtual_Display_Bytes, 320*200
    mov      Virtual_Display_Words, (320*200)/2
    mov      Virtual_Display_Dwords, (320*200)/4
    mov      eax, 13h
    int      10h

; * Allocate memory and setup the pointers
gr100_init:
      mov      eax, Virtual_Display_Bytes
      call     malloc_
      or       eax, eax                        ; See if allocation error
      jz       short derr
      mov      eax, VIDEO_START                ; debugging
      mov      Virtual_Display_Ptr, eax

      mov      ecx, Display_Height             ; ECX = number of lines
      mov      ebx, offset Line_Start_Table    ; EBX -> line start table
@@:   mov      [ebx], eax
      add      eax, Display_Width
      add      ebx, 4
      loop     @B
      mov      Virtual_Display_End, eax        ; -> the end of the buffer
dood: xor      eax, eax                    ; Clear out the virtual buffer
      mov      edi, Virtual_Display_Ptr
      mov      ecx, Virtual_Display_Dwords
      rep      stosd

      mov      Video_Access_Count, 1       
      mov      Update_Display_Flag, 1          ; Starts any updates

done: ret
derr: or       eax, -1
      jmp      done

gr_init_ endp


; ****************************************************************************
; * int GR_STOP( void )
; * Stops all graphics library processing
; * Given: nothing
; * Returns: 0 if all went well
; ****************************************************************************
gr_stop_ proc near uses ebx

      mov      Update_Display_Flag, 0      ; Stop any updates
      mov      Video_Access_Count, 0       
      mov      Lowest_X, -1
      mov      Lowest_Y, -1
      mov      Highest_X, 0
      mov      Highest_Y, 0

; * Free any allocated memory
      xor      eax, eax
      xchg     Virtual_Display_Ptr, eax    ; See if already setup
      or       eax, eax
      jz       short @F                        ; Skip if not

      call     free_

; * Restore timer interrupt
      cmp      Old_Timer_Vector, 0             ; timer already installed?
      jz       short @F

      mov      edx, Old_Timer_Vector
      mov      ecx, Old_Timer_Vector+4
      mov      eax, 205h                       ; EAX = DPMI Set Protected-mode
      mov      bl, TIMER_INTERRUPT             ;    Interrupt Vector command
      int      31h

; * Get the old video mode, if need be
@@:  cmp      byte ptr Old_Video_Mode, 0FFh   ; been here before?
     je       short @F

     mov      eax, Old_Video_Mode
     int      10h
     mov      byte ptr Old_Video_Mode, 0FFh

@@:  xor      eax, eax
     ret

gr_stop_ endp

; ****************************************************************************
; * int GR_SET_PIXEL( int EAX, int EDX, BYTE BL )
; * Sets a pixel in the virtual buffer
; * Given: EAX = X coordinate
; *     EDX = Y coordinate
; *     BL = pixel color
; * Returns: EAX = 0 if all went well only if range checking is enabled
; ****************************************************************************
gr_set_pixel_ proc near

ifdef RANGE_CHECK
     cmp      Virtual_Display_Ptr, 0      ; See if we are enabled
     jz       derr
     cmp      Display_Width, eax
     jbe      derr
     cmp      Display_Height, edx
     jbe      derr
endif

     cmp      Lowest_X, eax
     jb       short @F
     mov      Lowest_X, eax
@@:  cmp      Highest_X, eax
     ja       short @F
     mov      Highest_X, eax
@@:  cmp      Lowest_Y, edx
     jb       short @F
     mov      Lowest_Y, edx
@@:  cmp      Highest_Y, edx
     ja       short @F
     mov      Highest_Y, edx
@@:

     mov      edx, Line_Start_Table[edx*4]    ; EDX -> start of display line
     add      edx, eax
     mov      [edx], bl                       ; Put the pixel

     inc      Video_Access_Count
     ret

ifdef RANGE_CHECK
derr: or      eax, -1
      ret
endif

gr_set_pixel_ endp



; ****************************************************************************
; * int GR_RECT( int ECX, int EAX, int EDX, int ESI, BYTE BL )
; * Set a rectangle to a color
; * Given: ECX,EAX = X1,Y1 of upper left corner
; *     EDX,ESI = X2,Y2 of lower right corner, must be > EAX,EDX
; *     BL = color
; * Returns: EAX = 0 if all went well only if range checking is enabled
; ****************************************************************************
gr_rect_ proc near

      push     edi

ifdef RANGE_CHECK
      cmp      Virtual_Display_Ptr, 0      ; See if we are enabled
      jz       derr
      cmp      Display_Width, ecx
      jbe      derr
      cmp      Display_Width, edx
      jbe      derr
      cmp      Display_Height, eax
      jbe      derr
      cmp      Display_Height, esi
      jbe      derr
endif

      cmp      Lowest_X, ecx
      jb       short @F
      mov      Lowest_X, ecx
@@:   cmp      Highest_X, edx
      ja       short @F
      mov      Highest_X, edx
@@:   cmp      Lowest_Y, eax
      jb       short @F
      mov      Lowest_Y, eax
@@:   cmp      Highest_Y, esi
      ja       short @F
      mov      Highest_Y, esi
@@:

      inc      Video_Access_Count
      mov      edi, Line_Start_Table[eax*4] ; EDI -> start of display line
      add      edi, ecx                     ; EDI -> upperleft corner of screen

      sub      esi, eax          ; ESI = height - 1
      inc      esi

      mov      bh, bl            ; EAX = the color in all four bytes
      mov      eax, ebx
      shl      eax, 16
      mov      ax, bx

      sub      edx, ecx          ; EDX = width of the rectangle - 1
      inc      edx
      mov      ebx, Display_Width    ; EBX = width of the display in bytes
      sub      ebx, edx              ; EBX = wrap around value to add to 
                                     ;             EDI after each STOS
      ror      dx, 2       ; DL = width / 4 (will work up to 1023 pixels wide)
      shr      dh, 6       ; DH = width % 4 for remainder

      xor      ecx, ecx    ; Clear the MSWord of ECX

@@:   mov      cl, dl
      rep      stosd
      mov      cl, dh
      rep      stosb
      add      edi, ebx
      dec      esi
      jnz      @B

      pop      edi
ifdef RANGE_CHECK
      xor      eax, eax
endif
      ret

ifdef RANGE_CHECK
derr: or      eax, -1
      ret
endif

gr_rect_ endp

    end


Copyright © 1994, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.