Networking
From TorqueWiki
Contents |
[edit] Links
[edit] Networking MISC
http://tdn.garagegames.com/wiki/Code/How_Do_I_Manually_Set_a_ShapeBaseImage_State%3F
TCPObject Binary transfer Author: Dylan Sale (Dec 13, 2003) Categories: Mods Code File: filedownloader20060701.zip Allows the transfer of binary files over the TCPObject (and thus its subclasses): http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=4926
[edit] Multiplayer - Server Client
TGE Local Multiplayer: This resource implements local multiplayer support, allowing a client to control two or more players at the same time through a single connection: http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=10170
http://eviwo.free.fr/torque/multiplayer.html
http://www.mydreamrpg.com/community/showthread.php?t=122
http://tdn.garagegames.com/wiki/Torque/Networking/Server_and_Client
Pushbutton Master Server Redux: http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12801
Checking if the master server is available( in a thread): http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=11258
[edit] Datablocks
http://tdn.garagegames.com/wiki/TorqueScript_Quick_Reference_2#datablock
http://tdn.garagegames.com/wiki/Code/How_do_I_make_an_object_with_a_datablock%3F
http://tdn.garagegames.com/wiki/DTS/Scripting/Overview
Datablock Namespaces: http://tdn.garagegames.com/wiki/TorqueScript/Namespaces
http://tdn.garagegames.com/wiki/TorqueScript/Datablocks
http://www.garagegames.com/mg/forums/result.thread.php?qt=70230
[edit] Ghosts
http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=4852
http://tdn.garagegames.com/wiki/Torque/Networking/Ghosting
http://tdn.garagegames.com/wiki/Torque/Networking/Ghosting/Examples
[edit] Torque Network Library TNL
http://opentnl.sourceforge.net/doxytree/index.html
http://opentnl.sourceforge.net/doxytree/introprogramming.html
[edit] Basics
Torque was designed from the foundations up to offer robust client/server networked simulations.
Torque attempts to deal with three fundamental problems of network simulation programming:
- limited bandwidth
- packet loss
- latency
[edit] Bandwith
Bandwidth is a problem because in the large, open environments that Torque allows, and with the large number of clients that Torque supports (up to 128 per server, or beyond, if the network/server can handle it), potentially many different objects can be moving and updating at once.
Torque's main strategies to maximize available bandwith:
- Prioritize data: sending updates to what is most "important" to a client at a greater frequency than it updates data that is less important.
- Sending only data that is necessary.
- Using the BitStream class only the absolute minimum number of bits needed for a given piece of data.
- When object state changes, Torque only sends the part of the object state that changed.
- Torque caches common strings (NetStringTable) and data (SimDataBlock) so that they only need to be transmitted once.
[edit] Packet Loss
Packet loss is a problem because the information in lost data packets must somehow be retransmitted. Yet in many cases the data in the dropped packet shouldn't be resend, because a newer version already arrived, for example for the position of a player.
In order to minimize data that gets resent unnecessarily, the engine classifies data into four groups:
- Unguaranteed Data (NetEvent)
- if this data is lost, don't re-transmit it. An example of this type of data could be real-time voice traffic - by the time it is resent subsequent voice segments will already have played.
- Guaranteed Data (NetEvent)
- if this data is lost, resend it. Chat messages, messages for players joining and leaving the game and mission end messages are all examples of guaranteed data.
- Most-Recent State Data (NetObject)
- Only the most current version of the data is important - if an update is lost, send the current state, unless it has been sent already.
- Guaranteed Quickest Data (Move)
- critical data that must get through as soon as possible.
[edit] Latency
Latency is a problem in the simulation because the network delay in information transfer makes the client's view of the world perpetually out-of-sync with the server.
In order to solve these problems Torque employs several strategies:
- Interpolation is used to smoothly move an object from where the client thinks it is to where the server says it is.
- Extrapolation is used to guess where the object is going based on its state and rules of movement.
- Prediction is used to form an educated guess about where an object is going based on rules of movement and client input.
[edit] Client - Server
An instance of Torque can be set up as a dedicated server, a client, or both a client and a server. If the game is a client AND a server, it still behaves as a client connected to a server - instead of using the network, however, the NetConnection object has a short-circuit link to another NetConnection object in the same application instance.
[edit] Game Server and Multiplayer Setup
[edit] Firewall Ports
Allow: 192.168.1.0/16 Port 28000
StartMisssion -> hostmultiplayer
or for dedicatad server for mod "demo":
tge -game demo -dedicated -mission demo/data/missions/villa.mis
[edit] Database Connection
http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=7890
[edit] Platform Networking Layer (TCP/UDP)
The platform library provides the interface between the game engine and the OS dependent network functionality. The platform library's Net interface contains functions for opening reliable and unreliable communication sockets, converting between string and numeric network addresses and sending and receiving data.
Torque has good debugging capabilities. The DEBUG_NET and DEBUG_LOG() macros are used to control debug output from the networking code.
[edit] Connection Protocol
[edit] Connection Negotiation
The negotiation of a game network connection is not actually a part of the network class tree in the Torque - instead a set of functions, declared in engine/game/netDispatch.cc perform this service. The function DemoGame::processPacketReceiveEvent() is the main dispatch function for incoming network packets.
The first step of the connection process is the console function connect(), which initiates a connection attempt by sending a connect challenge request packet to the server from sendConnectChallengeRequest().
The server, in handleConnectChallengeRequest(), may issue the client a connect challenge response, which the client will process in handleConnectChallengeResponse. The client will in turn issue a connect request (sendConnectRequest) with the challenge information it received from the server. The server processes this message in handleConnectRequest. If the server decides to accept the request, it issues a sendConnectAccept back to the client and constructs a NetConnection object on the server to handle that client. The client, in handleConnectAccept creates a complementary NetConnection object to manage the client side of the connection. The dispatchCheckTimeouts function periodically checks if a connection request or challenge has been waiting too long and reissues the request if it has.
Once We're Connected...
Once a connection has been established, the function of the ConnectionProtocol class is to provide a common low-level mechanism for supporting the delivery of the four fundamental types of network data in the Torque. The ConnectionProtocol abstract base class implements a sliding window connected message stream over an unreliable transport (UDP). Rather than supporting guaranteed messages directly, the ConnectionProtocol class implements a notify protocol. Each packet sent is prepended with a message header containing tracking information, including what packets the other end of the connection has received or were dropped in transit. When a ConnectionProtocol instance determines that a packet it sent has been either received or dropped, it calls ConnectionProtocol::handleNotify(). Notifies are always delivered in the order packets were sent - so for every packet sent through a ConnectionProtocol object, eventually a notification of successful (ack) or unsuccessful (nack) delivery will be executed.
Because the base network protocol exports the inherently unreliable nature of the network to the simulation, at a higher level Torque can directly support different types of data guarantee: for unguaranteed data, if it is nacked, there is no need to resend it. For guaranteed data, if it is nacked, the engine queues it up for resend (NetConnection::eventPacketDropped()). If the data is most recent state data and the packet is nacked and that object's state hasn't been subsequently changed and resent, queue the data up for resend (NetConnection::ghostPacketDropped()). If the data is set for quickest possible delivery, continue sending the data with every packet until a packet containing the data is acked (GameConnection::readPacket()).
[edit] Datablocks
Datablocks (ie, subclasses of SimDataBlock) are used in the network system to store common instance data for objects. For example, a datablock may store animation data, model information, physical movement properties, etc, all of which are shared across a set of common objects. All declared datablocks are sent to clients upon connection as guaranteed events (SimDataBlockEvent), and can then be referenced and sent as part of the initial ghost update. An advantage of datablocks is that they are declared only on the server, so mods to the game can be created without forcing the client to downloading any script data.
[edit] Access to Datablock
[..] You are looking at the %obj.datablock field. This isn't an engine field that is going to get networked down to the client as part of any stock process. So if you are expecting the .datablock field to contain something on the client, you would have to make that happen.
The other note meant: if you are looking to get the datablock, you can use the getDatablock() command instead, that *is* a built-in engine function which is available to you.
How can the getDatablock() method work when referencing the datablock field doesn't? They refer to the same memory. One directly, one indirectly.
@Shon: Instead of comparing the datablock ID to string values (Which is what you are doing). When the datablock is transfered to the server it gets the ID of the datablock instead of its name.
...
%db = %obj.dataBlock;
switch$(%db.getName())
{
...
the code posted above fails because the datablock on the client does not have a name anymore, it just has a ghost id. On the client, you cannot compare the id of an object to its name like you can on the server. I believe Brian's suggestion will also fail on the client, because there, %db.getName() will always return "". (or something equally useless.)
If you want to add an identifier variable to a datablock that is carried over to the client, you'll have to explicitly add them in datablock's initPersistFields() method and transfer it to the client via stream writes and reads in it's packData() and unpackData() methods. If you're just adding fields via script it won't work.
[edit] Datablocks for shapebase DTS objects
How do I assign my DTS object to be a TSStatic or GameBase-derived object?
Which type it's assigned to is determined by two factors: how it is added to the game world (ie, mission file), and whether or not a datablock has been defined in the script that uses the shape model.
In the World Editor Creator, the object tree in the lower right contains two categories of interest: Shapes, and Static Shapes. Within each category is a tree of objects that belong to that category. How does the World Editor Creator make these categories?
-The "Shapes" category is compiled from all of the Datablocks that are defined in the game script belonging to GameBaseData or one of its derivatives (such as ShapeBaseData, ItemData, or even StaticShapeData...but I think PlayerData is excluded, for some reason).
-The "Static Shapes" category (not to be confused with the StaticShape class) is compiled from all of the DTS object files that it finds in your /mod/data folder (like in the /starter.fps/data folder, for example).
You'll find that DTS objects can appear in both lists. For example, in the starter.fps game, health kits appear in both the Shapes and Static Shapes categories. What you'll find is that the entry under Shapes (HealthKit) refers to the datablock-associated object, while the entry under Static Shapes (healthKit) is just the simple DTS file. When you add both to the game world, you'll find that the Shape health kit rotates and has all of the usual properties of the health kit (eg, can be picked up, used, etc), while the Static Shape health kit does nothing but sit there.
Also notice what happens in the object tree (upper right in the World Editor Creator) when you add each type of object to your game world. If you add the "Shape" health kit to the world, the object tree will list it as an Item. That's because the health kit shape is associated with a datablock, and the datablock is of the ItemData class (it's defined in /starter.fps/server/scripts/health.cs). If you add the "Static Shape" health kit to the world, the object tree will list it as a TSStatic. The latter is done because the engine is adding only the DTS object to the world, not a DTS-datablock combination, so it uses the simplest object form without a datablock (TSStatic).
[edit] Datablock Test Example
Define the datablock on the server:
datablock ItemData( GoldCoin )
{
category = "coins"; // For creator.
classname = "GoldCoin"; // Namespace.
goldCoinField = "gold";
};
Define on object using the datablock on the server:
%coin = new Item( serverGoldCoin )
{
datablock = GoldCoin;
serverField = "silver";
};
On the client you can create an object-instance that uses the datablock (you may call the testDatablock() function from the console):
function testDatablock()
{
echo("testDatablock ... start"); //ddd
%clientCoin = new Item( clientGoldCoin )
{
datablock = GoldCoin;
};
echo("\nYou may access the datablock via the object instance:");
echo ("___:" SPC clientGoldCoin );
echo ("___:" SPC clientGoldCoin.getDatablock() );
echo ("___:" SPC clientGoldCoin.getDatablock().goldCoinField );
echo("\nThe Datablock can also be accessed directly:");
echo ("___db GoldCoin:" SPC GoldCoin );
echo ("___db GoldCoin:" SPC GoldCoin.getId() );
echo ("___db GoldCoin:" SPC GoldCoin.goldcoinfield );
commandToServer( 'sendObjectGhostID', "serverGoldCoin" );
}
The last calls asked the server for the object created on the server and ghosted to the client.
The server function to answer the above call:
function serverCmdsendObjectGhostID(%targetClient, %serverObject)
{
echo("\nserverCmdsendObjectGhostID" SPC %targetClient SPC %serverObject );
// Get the GhostId that goes to the client.
%objIndex = %targetClient.getGhostID(%serverObject);
echo("_____________%objIndex:" SPC %objIndex);
%objIndex = %targetClient.getGhostID( serverGoldCoin );
echo("_____________%objIndex:" SPC %objIndex);
echo("\nThe game(server) connection is not the %targetClient.");
echo("_____________serverConnection:" SPC serverConnection );
echo("_____________serverConnection.getid:" SPC serverConnection.getid() );
echo("\nRe-Check the object id on the server for our Ghost.");
echo("Ghost ID %objIndex depends on client and object.");
%obj = %targetClient.resolveObjectFromGhostIndex( %objIndex );
echo("_____________%obj:" SPC %obj );
echo("_____________%obj.getId:" SPC %obj.getId() );
echo("___ =?=_______ Servergoldcoin.getID:" SPC servergoldcoin.getId() );
commandToClient(%targetClient, 'ReceiveGhost', %objIndex);
}
The client function for receiveing the answer from the server:
function ClientCmdReceiveGhost( %objIndex )
{
echo("\nThe client gets the ghost ID for him and obj");
echo("ClientCmdBar%objIndex (ghostID):" SPC %objIndex );
echo("______serverConnection:" SPC serverConnection.getId() );
echo("\nThe client uses its GameConnection (named serverConnection) to resove the GhostID");
%clientObject = serverConnection.resolveGhostID(%objIndex);
echo ("___%clientObject:" SPC %clientObject );
echo ("%clientObject.getId:" SPC %clientObject.getID() );
echo ("server field not networkable:" SPC %clientObject.serverField );
echo("\nOnly the Datablock of the server object is networked");
%db = %clientObject.getDatablock();
echo ("db__:" SPC %db );
echo ("db.field_:" SPC %db.goldCoinField );
//%clientObject.DoSomething();
}
[edit] Networking Classes
[edit] NetConnection
The NetConnection class is derivative from both SimGroup and ConnectionProtocol, and is responsible for managing the data streaming between client and server. The NetEvent class encapsulates the guaranteed and unguaranteed message delivery types and the ghost management portion of the NetConnection class handles state updates of world objects from server to client. The Torque example game-specific subclass of NetConnection is GameConnection and handles transmission of game specific data such as player moves.
The NetConnection class sends packets of a fixed size in a regular stream between the client and server. When a message is posted for transmission, it is aggregated with other messages and sent based on the packet rate and packet size settings for that connection.
NetConnection is the glue that binds a networked Torque game together. It combines the low-level notify protocol implemented in ConnectionProtocol with a SimGroup to provide a powerful basis for implementing a multiplayer game protocol.
On top of this basis it implements several distinct subsystems:
* Event manager, which is responsible for transmitting NetEvents over the wire. It deals with ensuring that the various types of NetEvents are delivered appropriately, and with notifying the event of its delivery status. * Move manager, which is responsible for transferring a Move to the server 32 times a second (on the client) and applying it to the control object (on the server). * Ghost manager, which is responsible for doing scoping calculations (on the server side) and transmitting most-recent ghost information to the client. * File transfer; it is often the case that clients will lack important files when connecting to a server which is running a mod or new map. This subsystem allows the server to transfer such files to the client. * Networked String Table; string data can easily soak up network bandwidth, so for efficiency, we implement a networked string table. We can then notify the connection of strings we will reference often, such as player names, and transmit only a tag, instead of the whole string. * Demo Recording is also implemented in NetConnection. A demo in Torque is a log of the network traffic between client and server; when a NetConnection records a demo, it simply logs this data to a file. When it plays a demo back, it replays the logged data. * The Connection Database is used to keep track of all the NetConnections; it can be iterated over (for instance, to send an event to all active connections), or queried by address.
[edit] The BitStream
BitStream is a utility class used to pack data for transmission. BitStream has methods for reading and writing variable-sized integers, floats, vectors, Huffman-coded strings and bits.
[edit] Network Events
The NetEvent class provides a foundation for guaranteed, ordered and unguaranteed message transmission.
[edit] NetObject
Introduction To NetObject And Ghosting One of the most powerful aspects of Torque's networking code is its support for ghosting and prioritized, most-recent-state network updates. The way this works is a bit complex, but it is immensely efficient. Let's run through the steps that the server goes through for each client in this part of Torque's networking:
* First, the server determines what objects are in-scope for the client. This is done by calling onCameraScopeQuery() on the object which is considered the "scope" object. This is usually the player object, but it can be something else. (For instance, the current vehicle, or a object we're remote controlling.) * Second, it ghosts them to the client; this is implemented in netGhost.cc. * Finally, it sends updates as needed, by checking the dirty list and packing updates.
There several significant advantages to using this networking system:
* Efficient network usage, since we only send data that has changed. In addition, since we only care about most-recent data, if a packet is dropped, we don't waste effort trying to deliver stale data. * Cheating protection; since we don't deliver information about game objects which aren't in scope, we dramatically reduce the ability of clients to hack the game and gain a meaningful advantage. (For instance, they can't find out about things behind them, since objects behind them don't fall in scope.) In addition, since ghost IDs are assigned per-client, it's difficult for any sort of co-ordination between cheaters to occur.
NetConnection contains the Ghost Manager implementation, which deals with transferring data to the appropriate clients and keeping state in synch.
[edit] Network Ghosts and Scoping
[edit] On Ghosting and Scoping
Ghosting is the most complex, and most powerful, part of Torque's networking capabilities. It allows the information sent to clients to be very precisely matched to what they need, so that no excess bandwidth is wasted. The control object's onCameraScopeQuery() is called, to determine scoping information for the client; then objects which are in scope are then transmitted to the client, prioritized by the results of their getPriority() method.
There is a cap on the maximum number of ghosts; ghost IDs are currently sent via a 10-bit field, ergo, there is a cap of 1024 objects ghosted per client. This can be easily raised; see the GhostConstants enum.
Each object ghosted is assigned a ghost ID; the client is _only_ aware of the ghost ID. This acts to enhance game security, as it becomes difficult to map objects from one connection to another, or to reliably identify objects from ID alone. IDs are also reassigned based on need, making it hard to track objects that have fallen out of scope (as any object which the player shouldn't see would).
resolveGhost()
is used on the client side, and
resolveObjectFromGhostIndex()
on the server side, to turn ghost IDs into object references.
Since the server is the only side that has knowledge of both the GhostID and the actual ObjectID it is mapped for, the developer is responsible for managing any communcations between client and server that involve Object/GhostID's. The networked updates takes care of this automatically, but if you are using any RPC command functionality (commandToServer, commandToClient, etc.), then you will need to make sure that the ID numbers are in synch.
Within the source code, we have two functions for this purpose:
* NetConnection::resolveGhost--on the client, this command returns a pointer to a NetObject that is indexed by a GhostID. * NetConnection::resolveObjectFromGhostIndex--on the server, this command returns a pointer to a NetObject that is indexed by a GhostID (supplied by the client in some manner).
In script, we have three ConsoleMethods that do similar work for us, but deal only in ObjectIDs instead of pointers:
* resolveGhostID--on the client, this console method returns an ObjectID that is associated with a GhostID sent from the server. * resolveObjectFromGhostIndex--on the server, this console method returns an ObjectID that is associated with a GhostID sent from a client. * getGhostID--on the server, this console method returns the GhostID associated with a server side simulation object for this connection
[edit] NetObject
The NetConnection is a SimGroup. On the client side, it contains all the objects which have been ghosted to that client. On the server side, it is empty; it can be used (typically in script) to hold objects related to the connection. For instance, you might place an observation camera in the NetConnnection. In both cases, when the connection is destroyed, so are the contained objects.
NetObject is the superclass for ghostable objects,
The NetObject class is a derivative of SimObject that can replicate (ghost) itself across a network connection. All world object classes are subclassed from NetObject (the superclass of SceneObject). In order to best utilize the available bandwidth, the NetConnection attempts to determine which objects are "interesting" to each client - and among those objects, which ones are most important. If an object is interesting to a client it is said to be "in scope" - for example, a visible enemy to a player in a first person shooter would be in scope.
Each NetConnection object maintains a scoping object - responsible for determining which objects are in scope for that client. Before the NetConnection writes ghost update information into each packet in NetConnection::ghostWritePacket(), it calls the scope object's onCameraScopeQuery() function which performs two services: first, it determines which objects are "in scope" for that client and calls NetConnection::objectInScope for each object on that client. Second, the onCameraScopeQuery() call fills in the CameraScopeQuery structure which is then used to determine the priority of object updates.
Rather than always sending the full state of the object each time it is updated across the network, the Torque supports only sending portions of the object's state that have changed. To facilitate this, each NetObject can specify up to 32 independent sub-states that can be modified individually. For example, a player object might have a movement state, detailing its position and velocity, a damage state, detailing its damage level and hit locations, and an animation state, signifying what animation, if any, the player is performing.
Each state data group is assigned a bit position in the class. When an object's state changes, the object notifies the network system with the NetObject::setMaskBits function. When the object is to be written into a packet in NetObject::packUpdate, the object's current state mask is passed in. The object's state mask is NOT written into the packet directly - it is the responsibility of the pack function to accurately encode which states are updated.
Initially an object's state mask is set to all 1's - signifying that all the object's states need to be updated.
[edit] GameConnection, Moves and the Control Object
GameConnection is the game-specific subclass of NetConnection. Applications can subclass NetConnection to directly write and read data from packets, as well as hook into the notify mechanism.
The GameConnection in the Torque example introduces the concept of the control object. The control object is simply the object that the client associated with that network connection controls. By default in the example the control object is an instance of the Player class, but can also be an instance of Camera (when editing the mission, for example).
[edit] Simulating the Control Object
aster of the simulation. To prevent clients from cheating, the server simulates all player moves and then tells the client where his player is in the world. This model, while secure, can have problems - if the network latency is high, this round-trip time can give the player a very noticeable sense of movement lag. To correct this problem, the example uses a form of prediction - it simulates the movement of the control object on the client and on the server both. This way the client doesn't need to wait for round-trip verification of his moves - only in the case of a force acting on the control object on the server that doesn't exist on the client does the client's position need to be forcefully changed.
To support this, all control objects (derivative of ShapeBase) must supply a writePacketData() and readPacketData() function that send enough data to accurately simulate the object on the client. These functions are only called for the current control object, and only when the server can determine that the client's simulation is somehow out of sync with the server. This occurs usually if the client is affected by a force not present on the server (like an interpolating object) or if the server object is affected by a server only force (such as the impulse from an explosion).
The Move structure is a 32 millisecond snapshot of player input, containing x, y, and z positional and rotational changes as well as trigger state changes. When time passes in the simulation moves are collected (depending on how much time passes), and applied to the current control object on the client. The same moves are then packed over to the server in GameConnection::writePacket(), for processing on the server's version of the control object.
[edit] NetStringTable
The NetStringTable class manages string data across connections. Every tagged string in the console - those enclosed by single quotes ('), will be sent across a connection only a single time. Every subsequent time that string is sent, an integer tag is substituted for the actual string data. Strings like player names can be added with the addTaggedString console function and removed with the removeTaggedString console function.
[edit] Network Console Commands
There are two remote procedure call network console commands - commandToServer and commandToClient. The commandToServer function takes the form:
commandToServer(functionNameTag, arg1, arg2, arg3, ... )
where functionNameTag is some string tag. This call is converted into a RemoteCommandEvent and set across to the server. Once there the server calls the local script function
serverCmdXXX(clientId, arg1, arg2, arg3, ... )
where XXX is the text of the string tag. The commandToClient function takes the form:
commandToClient(clientId, functionNameTag, arg1, arg2, arg3, ... )
where the clientId argument is the object id of the connection object to send to.
The commandTo* functions perform string argument substitution automatically using the in-string % modifier. For example:
commandToClient('EchoMessage',
'This %1 guy is super %2',
'Got Milk?',
'slow at writing documentation');
is executed on the client as:
function clientCmdEchoMessage(%message, %a1, %a2, %a3, %a4)
{
// tagged strings must be detagged in order to be displayed.
echo(detag(%message));
echo("a1 = " @ detag(%a1));
echo("a2 = " @ detag(%a2));
echo("a3 = " @ detag(%a3));
echo("a4 = " @ detag(%a4));
}
and would echo:
This Got Milk? guy is super slow at writing documentation a1 = Got Milk? a2 = slow at writing documentation a3 = a4 =
The string substitution number (after the %) refers to the argument position n spaces after the current argument:
CommandToClient('EchoMessage',
'%1 is a good %2 for %3',
'%1 the good %2',
'Role Model',
'SuperDood %1',
'the dude of super');
Would echo:
Role Model the good SuperDood the dude of super is a good Role Model for SuperDood the dude of super A1 = Role Model the good SuperDood the dude of super A2 = Role Model A3 = SuperDood the dude of super A4 = the dude of super
This functionality is especially useful for status and game messages coming from the server, because each text message compresses into just a small array of tag identifiers.
