New network protocols or connection methods can be supported by writing new driver DLL's. A separate DLL must be written for the desired communication medium and protocol for each platform you wish to support - DOS, Win32, Mac, or Unix. For instance, under DOS, we wrote a new transport DLL to support Dwango, and under Windows, we wrote a loopback driver which lets you develop games without having to connect to a second computer.
Once a game has been selected, the shell shuts down the comm DLLs and exits, and the game program itself starts. (This is handled by dpLaunchApp(). In DOS, dpLaunchApp() tells the stub to launch the game program, then terminates itself; in Windows and Mac, dpLaunchApp() directly launches the game program, then teriminates itself). The game program loads the same comm DLLs as the shell, plays the game, shuts down the comm DLLs, and exits. The stub starts the shell again and the user is prompted to join another game.
The comm DLLs cannot persist across invocations of the shell and the game program because the shell exits before the game program starts up, and vice versa. The dpio layer has a function dpio_freeze() which writes out enough information to set up the connections again; the function dpio_create() can read that file and reinitialize the comm DLL's.
A trick will be used on Win95 to keep the handle to the modem's comm port open - it will be marked as inheritable by the netshell, and the value of the handle will be written to disk by dpio_freeze(). A similar trick will also be needed on the Mac for the modem driver.
Games can also run standalone without the stub and netshell, but they then have to implement those functions themselves.
The actual way in which a message is transmitted is defined by the transport layer. For instance, the point-to-point null-modem DLL supplied with Anet performs its own data compression and error correction.
The whole point of the transport DLL is the pair of functions
CommTxPkt and CommRxPkt.
The upper layer sends a message by calling the transport
layer's CommTxPkt routine.
Each arriving message is queued up for retrieval
by the upper layer. The upper layer retrieves a message (and
the player handle of the node which sent it) by calling the DLL's
CommRxPkt routine.
The transport layer cannot handle an unlimited number of simultaneous messages. Therefore, it is possible for messages to be dropped silently, if memory begins to run low.
Note: these really should be called node handles.
Communication between the dpio layer and the transport layer is done in terms of player handles. A player handle is an integer. Player handles are assigned dynamically by the transport layer and are valid only until the transport layer is unloaded (typically when the upper layer exits).
Two nodes may refer to a third node via player handles which are not identical. That is, player handles are assigned independently by each node; often, the first node you connect to gets handle 1, the second node gets handle 2, etc. But some drivers assign handles differently, e.g. the UUDP driver under Linux returns the IP address as the handle.
The transport layer learns about nodes in only one way: when the upper layer calls the commSayHi function. If a packet has been received from a node for which a player handle has not yet been allocated with commSayHi, the source node will be identified as PLAYER_UNKNOWN. The upper layer should get the address of the current node using commPlayerInfo, then embed that address in a packet, and broadcast it (or send it to a well-known game server). When packets containing addresses are received, the upper layer can call commSayHi if it wishes to send packets to the nodes which advertized their address.
The predefined handles include:
In some cases, such as a local IP network, two nodes can communicate without any setup. In other cases, such as a modem-to-modem link, a node cannot communicate with another node until it has dialed the phone. In still other cases, such as a dial-up hub service, it is possible for a node with a modem to communicate with two or more other nodes without hanging up and redialing.
If the DLL has not assigned a player handle to the destination node, the upper layer cannot yet use CommTxPkt, since CommTxPkt expects a player handle. There are at least four ways around this situation:
Disconnecting from predefined handles is not allowed.
However, users may need to view or edit connection information, which means it must be presented in a readable format. Certain communications media cannot even be bootstrapped until connection information is provided by the user. For instance, without a phone number to dial, a modem transport layer cannot originate even a single connection.
To format a connection information block into a readable string, the upper layer can call the DLL's CommPrintAddr routine. To convert readable data to a connection information block, the upper layer can call the DLL's CommScanAddr routine.
In that sense, it is like a file I/O package, which reads and writes data and refers to its targets (files) via handles. Neither the file I/O package nor the transport layer is particularly concerned with the contents of the data streams it manipulates.
The following is a brief synopsis of the steps needed to establish a game using Anet's transport DLLs.
Note: this is normally left to the DP package. The following section is mostly of interest for people trying to understand how DP and dpio use the comm dll's.
The pre-executable launches the shell, passing it a command-line switch indicating the selected comm medium and protocol. The shell reads this switch, loads the appropriate comm DLL using dpLoadDLL(), and calls the DLL's CommInit routine, to perform DLL-specific initializations.
If the CommInit routine returns a failure status, the DLL is unusable and the shell complains and exits.
Since the concept of broadcasting is protocol-dependent, the transport layer sends out the "who's there" message in whatever manner, and to whatever destination(s), it chooses. In some cases, such as modem, the message may go to at most a single node. In other cases, such as IP, the message may be broadcast over a local network.
When a "who's there" message arrives at a node, the transport layer passes it up to the shell. Any node's shell can respond to a "who's there", but typically only the hosts of games will do so. If a non-host responds, this is usually because it suspects that the host of its game is not directly reachable by the sender of the "who's there."
It is not necessary for every node that hears the "who's there" to respond, but those that do not respond run the risk of never being contacted by the sender again (not always a disadvantage).
To respond to a "who's there" message, a node's shell sends an "I'm here" message. The "I'm here" message may include such information as whether the node is hosting a game (and the name of the game), other media supported by the node, and the node's physical location.
Over time, the shell acquires a list of active game hosts, which it can present to the user. If the user selects a host to query, the shell can transmit messages to that host to get further information (such as the number of available spaces on the game, the mission, and the names of the other players). It may also attempt to contact each of the other players in that game, in an effort to determine whether throughput and connectivity will hinder game play.
Finally, the user selects a game to play. The shell sends a message to the host to indicate that it wants to join the game, and the host responds (presumably in the affirmative). Before the user's shell exits, it saves the connection information for the host (and for as many other nodes as it needs to, including non-players) in a file and unloads the transport layer DLL. When the game is launched, it reloads the transport layer DLL, reads the connection information back in from the file, establishes connections to as many of the nodes as it needs to, and begins the game.
However, many routines require access to system-wide resources. For example, malloc() expects that DOS has given it a pointer to available memory. Since DOS knows nothing about the DLL, this will fail.
To get around many of these problems, the DLL can include a jump table which vectors into the various entry points in the executable's version of the C runtime library. That is, to call malloc(), the DLL would actually call a thunk of code which jumps through the jump table into the malloc() in the C runtime library linked in by the executable that loaded the DLL.
There are a few exceptions to this, however. The most notable is file I/O, which does not work because it depends on startup code (which is never run in the DLL) and on __iob[], a constant.
The CommDriverInfo routine returns dynamic information about the DLL.
The CommPlayerInfo routine returns dynamic information about the given player handle.
There is a way to report scores to the underlying network. See the Dwango driver and dpReportScore for an example.
On the Mac, Howard Shere implemented a scheme whereby the transport DLL was passed a screen rectangle which it could use, and used this for the user interface associated with CommToolbox calls. This scheme currently lies dormant in the Mac code for the modem and serial drivers; when we try to get those drivers working again, we might need to add a screen rectangle and/or window handle field into the commInitReq_t passed to commInit(). This needs to be better defined, probably with help from the fellow doing the Netshell.