GameDesign: Difference between revisions
No edit summary |
imported>Blacklads Undo revision 11673 by Ufizavipupu (Talk) |
||
| (154 intermediate revisions by 6 users not shown) | |||
| Line 1: | Line 1: | ||
{{Navigation for Marauroa Top|Internals}} |
|||
!!! Database Tables and Relationships |
|||
{{Navigation for Marauroa Developers}} |
|||
Last updated: 2003/10/07 |
|||
The database table relationship schema is: |
|||
<verbatim> |
|||
Tables: |
|||
--------- |
|||
= Basic idea behind GameManager = |
|||
Table PLAYER |
|||
The idea behind the Game Manager is to handle all the "business logic". This Manager decides how to reply to each individual message. |
|||
{ |
|||
PK(username) |
|||
password |
|||
} |
|||
= GameManager = |
|||
Table CHARACTERS |
|||
The logic is similar to this: |
|||
<pre> |
|||
GameManager |
|||
{ |
{ |
||
NetworkManager read Message |
|||
PK(character) |
|||
content |
|||
} |
|||
switch(Message type) |
|||
Table LOGIN_EVENT |
|||
{ |
{ |
||
case ...; |
|||
PK(id) |
|||
} |
|||
timedate |
|||
result |
|||
} |
} |
||
</pre> |
|||
So let's define the reply to each message. First, let's clarify that the best way of modelling this system is using finite automates, (a finite state machine) where, based on the input, we change the state we are currently in and produce an output. |
|||
Table STATISTICS |
|||
== Login stage == |
|||
( |
|||
<b>NOTE</b>: This stage has been split in 3 to allow proper secure login. |
|||
PK(timedate) |
|||
<b>NOTE</b>: Explain here how secure login works. |
|||
<pre> |
|||
bytes_send |
|||
Process C2S Login ( STATE_BEGIN_LOGIN ) |
|||
bytes_recv |
|||
Precondition: The state MUST be NULL |
|||
Test if there is room for more players. |
|||
players_login |
|||
if there is no more room |
|||
players_logout |
|||
{ |
|||
players_timeout |
|||
reply S2C Login NACK( SERVER_FULL ) |
|||
players_online |
|||
state = NULL |
|||
) |
|||
} |
|||
if check username, password in database is correct |
|||
Table RPOBJECT |
|||
{ |
|||
create clientid |
|||
PK(id) |
|||
add PlayerEntry |
|||
slot_id |
|||
notify database |
|||
) |
|||
reply S2C Login ACK |
|||
Table RPATTRIBUTE |
|||
( |
|||
PK(object_id) |
|||
PK(name) |
|||
value |
|||
) |
|||
get characters list of the player |
|||
Table RPSLOT |
|||
reply S2C CharacterList |
|||
( |
|||
object_id |
|||
name |
|||
PK(slot_id) |
|||
) |
|||
state = STATE_LOGIN_COMPLETE |
|||
} |
|||
else |
|||
{ |
|||
notify database |
|||
reply S2C Login NACK( LOGIN_INCORRECT ) |
|||
Relationships: |
|||
state = NULL |
|||
---------- |
|||
} |
|||
Relationship PLAYER_CHARACTERS |
|||
{ |
|||
PK(player_username) |
|||
PK(characters_character) |
|||
} |
|||
Postcondition: The state MUST be NULL or STATE_LOGIN_COMPLETE |
|||
Relationship PLAYER_LOGIN_EVENT |
|||
and a we have created a PlayerEntry for this player with a unique clientid. |
|||
{ |
|||
PK(player_username) |
|||
PK(login_event_id) |
|||
} |
|||
</verbatim> |
|||
So we can translate this to SQL easily and we should create the following SQL Queries: |
|||
<verbatim> |
|||
CREATE TABLE player |
|||
( |
|||
id BIGINT PRIMARY KEY NOT NULL, |
|||
username VARCHAR(30) NOT NULL, |
|||
password VARCHAR(30) NOT NULL |
|||
); |
|||
</pre> |
|||
CREATE TABLE characters |
|||
( |
|||
player_id BIGINT NOT NULL, |
|||
charname VARCHAR(30) NOT NULL, |
|||
contents VARCHAR(4096) |
|||
== Choose character stage == |
|||
<pre> |
|||
); |
|||
Process C2S ChooseCharacter ( STATE_LOGIN_COMPLETE ) |
|||
Precondition: The state MUST be STATE_LOGIN_COMPLETE |
|||
if character exists in database |
|||
CREATE TABLE loginEvent |
|||
{ |
|||
add character to Player's PlayerEntry |
|||
player_id BIGINT NOT NULL, |
|||
add character to game |
|||
address VARCHAR(20), |
|||
reply S2C Choose Character ACK |
|||
timedate TIMEDATE, |
|||
result TINYINT |
|||
); |
|||
state = STATE_GAME_BEGIN |
|||
} |
|||
else |
|||
{ |
|||
reply S2C Choose Character NACK |
|||
state = STATE_LOGIN_COMPLETE |
|||
} |
|||
Postcondition: The state MUST be STATE_GAME_BEGIN and the PlayerStructure |
|||
CREATE TABLE statistics |
|||
should be completely filled or if the character choise was wrong the state is STATE_LOGIN_COMPLETE |
|||
( |
|||
timedate TIMESTAMP, |
|||
</pre> |
|||
bytes_send INTEGER, |
|||
bytes_recv INTEGER, |
|||
== Logout stage == |
|||
players_login INTEGER, |
|||
<pre> |
|||
players_logout INTEGER, |
|||
players_timeout INTEGER, |
|||
players_online INTEGER, |
|||
Process C2S Logout ( STATE_GAME_END ) |
|||
PRIMARY KEY(timedate) |
|||
Precondition: The state can be anything but STATE_LOGIN_BEGIN |
|||
); |
|||
if( rpEngine allows player to logout ) |
|||
CREATE TABLE rpobject |
|||
{ |
|||
reply S2C Logout ACK |
|||
id INTEGER NOT NULL PRIMARY KEY, |
|||
state = NULL |
|||
slot_id INTEGER |
|||
); |
|||
store character in database |
|||
CREATE TABLE rpattribute |
|||
remove character from game |
|||
( |
|||
delete PlayerEntry |
|||
object_id INTEGER NOT NULL, |
|||
} |
|||
name VARCHAR(64) NOT NULL, |
|||
else |
|||
value VARCHAR(255), |
|||
{ |
|||
PRIMARY KEY(object_id,name) |
|||
reply S2C Logout NACK |
|||
); |
|||
} |
|||
Postcondition: Either the same as the input state or the state currently in |
|||
CREATE TABLE rpslot |
|||
( |
|||
object_id INTEGER NOT NULL, |
|||
name VARCHAR(64) NOT NULL, |
|||
slot_id INTEGER AUTO_INCREMENT NOT NULL, |
|||
</pre> |
|||
PRIMARY KEY(slot_id) |
|||
); |
|||
</verbatim> |
|||
== Perception confirmation stage == |
|||
!!! JDBC Database HOWTO |
|||
<pre> |
|||
Last updated: 2003/10/23 |
|||
Process C2S Perception ACK |
|||
JDBC technology is an API that lets you access virtually any tabular data source from the Java programming language. It provides cross-DBMS connectivity to a wide range of SQL databases, and now, with the new JDBC API, it also provides access to other tabular data sources, such as spreadsheets or flat files. |
|||
Precondition: The state must be STATE_LOGIN_BEGIN |
|||
notify that the player received the perception. |
|||
JDBCPlayerDatabase is anyway not database independent; on the Player table we are using AUTOINCREMENT that is a unique keyword of MySQL that is not part of the SQL standard. |
|||
Postcondition: The state is STATE_LOGIN_BEGIN and Timestamp field in |
|||
You need to download MySQL Connector/J in order to get it to run. http://www.mysql.com/downloads/api-jdbc-stable.html. |
|||
PlayerContainer is updated. |
|||
</pre> |
|||
To configure Marauroa to work with a JDBC source we need to modify the configuration of the JDBC Connection. |
|||
== Transfer confirmation stage == |
|||
So open marauroad.ini file and edit the next fields |
|||
<pre> |
|||
<verbatim> |
|||
marauroa_DATABASE=JDBCPlayerDatabase |
|||
Process C2S Transfer ACK |
|||
jdbc_class=com.mysql.jdbc.Driver |
|||
Precondition: The state must be STATE_LOGIN_BEGIN |
|||
jdbc_url=jdbc:mysql://localhost/marauroa |
|||
jdbc_user=marauroa_dbuser |
|||
jdbc_pwd=marauroa_dbpwd |
|||
</verbatim> |
|||
jdbc_class is the field that says what Driver to use. Please refer to your software manual to see the multiple options. |
|||
foreach content waiting for this player |
|||
jdbc_url points to the type and source of the information, for MySQL the string must be as follow: |
|||
{ |
|||
if client acked it |
|||
{ |
|||
send content to client |
|||
} |
|||
} |
|||
Postcondition: The state is STATE_LOGIN_BEGIN and the content waiting for player is clear. |
|||
jdbc:mysql://[:]/ |
|||
</pre> |
|||
=PlayerContainer Explained= |
|||
jdbc_user is the username for the database and jdbc_pwd is the password for that username in the database. |
|||
PlayerContainer is the data structure that contains all of the information about the players while the game is running. |
|||
It consists of a list of RuntimePlayerEntry objects and is heavily linked with the PlayerDatabase, so we can hide the complexity to GameManager. By making PlayerDatabase hidden by PlayerContainer we achieve the illusion that managing the runtime behavior we modify automatically the permanent one. |
|||
Then simply save the changes and ready. |
|||
RuntimePlayerEntry is the structure that contains the information about the player while it is online. <br>RuntimePlayerEntry contains: |
|||
Before using the application with the database, you need to create the database itself. So in case of MySQL just open MySQL and write: |
|||
* <i>clientid</i><br> |
|||
Clientid is the field that indexes players in the server. See the documentation about clientid generation to understand what they are and how they are generated. |
|||
<verbatim> |
|||
* <i>source</i><br> |
|||
create database marauroa; |
|||
Source is the IPv4 address of the client. This is used to determine if the message is really coming from the client or another person trying to impersonate it. |
|||
grant all on marauroa.* to marauroa_dbuser@localhost identified by 'marauroa_dbpwd'; |
|||
* <i>timestamp</i><br> |
|||
</verbatim> |
|||
Timestamp is used to determine if a client has timed out in which case it is only wasting resources on the server. As you may already know, UDP is not a delivery-guaranteed protocol, so we need to check for dead clients ourselves. Note that this only indicates that the player timed out and it doesn't apply any kind of measures on them. |
|||
* <i>storedTimestamp</i><br> |
|||
The rest of code is handled by the server itself, and will create the tables if they don't exits. |
|||
storeTimestamp is used to determine when the player was last stored in the database. We don't store each time the player info changes as this would obviously be very CPU time consuming. Instead we cached the changes and store them only every 5 minutes. |
|||
* <i>username</i><br> |
|||
!!! Player Container Explained |
|||
Username is filled in at runtime with a Login event. If we store the username here we are able to use the database from PlayerContainer thus by knowing the clientid we can also now know the username without having to look to the actual database. |
|||
Last updated: 2003/10/23 |
|||
* <i>choosenCharacter</i><br> |
|||
choosenCharacter is filled in at runtime with a ChooseCharacter event. If we store the information here we are able to use the database from PlayerContainer and hence by knowing the clientid we also know the choosenCharacter without having to refer to the actual database. |
|||
~PlayerContainer is the data structure that contains all of the needed info about the players to keep the game running. |
|||
* <i>state</i><br> |
|||
It consists of a list of ~RuntimePlayerEntry objects, and is heavily linked with the ~PlayerDatabase, so we can hide the complexity to ~GameManager. By making ~PlayerDatabase hidden by ~PlayerContainer we achieve the illusion that managing the runtime behavior we modify automatically the permanent one. |
|||
~RuntimePlayerEntry is the structure that contains the information of the player while it is online. ~RuntimePlayerEntry contains: |
|||
clientid |
|||
Clientid is the field in charge of indexing players in the server. See the document about clientid generation to understand what they are and how they are generated. |
|||
source |
|||
Source is the IPv4 address of the client, so that we can determine if the message is really coming from client or another person trying to impersonate it. |
|||
timestamp |
|||
Timestamp is used to determine if a client has timed out and as such, it is only wasting resources on the server. As you know, UDP is not a delivery-guaranteed protocol, so we need to check ourselves for dead clients. Take care that it only indicates that the player timed out, it doesn't apply any kind of measures over them. |
|||
username |
|||
Username is filled in at runtime with a Login event so that we are able to use the database from ~PlayerContainer, This way by knowing the clientid we can also know the username. |
|||
choosenCharacter |
|||
choosenCharacter is filled in at runtime with a ~ChooseCharacter event so that we are able to use the database from ~PlayerContainer, This way by knowing the clientid we can also know the choosenCharacter. |
|||
state |
|||
State is a number expressing the state in which the player is. There are four states: |
State is a number expressing the state in which the player is. There are four states: |
||
Have to login |
*Have to login |
||
Login Complete |
*Login Complete |
||
Game begin |
*Game begin |
||
Logout |
*Logout |
||
When we create the entry it is by default Have to login. Once you have logged in correctly, we change state to Login Complete, and once you have chosen a Character we change it to game begin. The logout state is trivial :) |
|||
The main idea is that some operations are only allowed in one state, so we can more easily control it with the state property. |
|||
perception counter |
|||
Perception counter is used for having a incremental counter of the perceptions send, so that client can see if it gets out of sync. |
|||
perception Previous RPObject |
|||
Perception previous RPObject is the RPObject that was sent on the last perception, so we can track changes on our RPObject without disturbing the rest of the system. |
|||
timestamp of the Last time it was stored |
|||
Timestamp of the Last time it was stored means the timestamp of the moment in which the object was stored at Database. |
|||
As you can see all we need to operate ~PlayerDatabase is a username and choosenCharacter. So using ~PlayerEntryContainer we can fully operate it. |
|||
!!! ClientID generation |
|||
Last updated: 2003/10/23 |
|||
Each client MUST have a session id to avoid another player to impersonate it. sessionid must be a short or int to make harder for an attacker to guess it. |
|||
To make it really fun, clientids are generated randomly for each player with the unique condition that two different players MUST have two different clientids. |
|||
Home |
|||
!!! Synchronization between Game and RP Managers |
|||
Last updated: 2003/12/06 |
|||
Why bother with it? Well, imagine that a player logs out when the perception is being built, it will no longer be accessible for the RP Manager, when it really expects the object to be there. Or a removed player that is removed too by RP Manager. That is a really serious problem, as it will make the server fail. |
|||
When we create the entity, by default, the state is <b>Have to login</b>. Once you have logged in correctly, the state changes to <b>Login Complete</b> and once the player has chosen a Character it changes to <b>game begin</b>. The logout state is pretty trivial :) |
|||
So we need to synchronize game and RP manager. |
|||
The idea is that some operations are only allowed in certain states, so the state property stores which state they are in to make validating actions easier. ( To read about Perceptions, [[RolePlayingDesign#Perceptions | click here]] ) |
|||
The idea is that they request to a central mutex access to the ~PlayerEntryContainer, and that mutex is the one that decide how the access is done. |
|||
*perception counter<br> |
|||
We need to differentiate between the two types of accesses, read access and write access. We can have without problems two readers accessing in parallel, but we can only have one write at the same time modifying the stuff. |
|||
The Perception counter is used to keep an incremental count of the perceptions sent so that the client can see if it gets out of sync. |
|||
*perception Previous RPObject<br> |
|||
Perception previous RPObject is the RPObject that was sent on the last perception. Using this we can track changes to a RPObject without disturbing the rest of the system. |
|||
*perception Out of Sync<br> |
|||
This flag indicates to the server if the player has become out of sync. This allows us to re-sync it as soon as possible. |
|||
Hence, all we need to operate PlayerDatabase is a username and choosenCharacter. So using PlayerEntryContainer we can fully operate it. |
|||
Whatever action we choose in ~GameManager they are Write actions, as the modify the state of the ~PlayerContainer, but in RP we have two parts, one that build the perceptions that is read only and one that removes idle players that is write, so we must apply two different locks there. |
|||
Home |
|||
==ClientID generation== |
|||
!!! Persistent Objects ER |
|||
Each client MUST have a session id to prevent another player impersonating it. sessionid must be of short or int size to make guessing the ID much harder. |
|||
Last updated: ? |
|||
To make it even more secure, clientids are generated randomly for each player with the only condition that two different players MUST have two different clientids. |
|||
use marauroa; |
|||
<verbatim> |
|||
drop table rpobject; |
|||
drop table rpattribute; |
|||
drop table rpslot; |
|||
drop table RPObjectInRPSlot; |
|||
==Synchronization between Game and RP Managers== |
|||
CREATE TABLE rpattribute ( |
|||
Why bother with this? Well, imagine that a player logs out while the perception is being built, it will no longer be accessible by the RP Manager when it expects the object to be there, or if RPManager tries to remove a player which has already been removed, these situations are very serious as they will probably make the server fail. |
|||
object_id int(11) NOT NULL default '0', |
|||
name varchar(64) NOT NULL default '', |
|||
value varchar(255) default NULL, |
|||
PRIMARY KEY (object_id,name) |
|||
) TYPE=InnoDB; |
|||
So we must synchronize the Game and RP Managers. |
|||
The idea behind the solution is that the each manger requests access to the PlayerEntryContainer via a central mutex (a mutex is a syncronisation element attached to a resource, which can be owned by one task at any point in time. If the mutex is owned already when a task tries to access the object protected by it then the mutex will inform the task that it doesn't have access at this point in time to the object). |
|||
CREATE TABLE rpslot ( |
|||
object_id int(11) NOT NULL default '0', |
|||
name varchar(64) NOT NULL default '', |
|||
slot_id int(11) NOT NULL auto_increment, |
|||
PRIMARY KEY (slot_id) |
|||
) TYPE=InnoDB; |
|||
There are two types of accesses, read accesses and write accesses. Note that two readers can access an object in parallel but only one write can happen at the same time. |
|||
In GameManager there are only Write actions, as this manager only modifies the state of the PlayerContainer. However, in the RP manager we have both Reads, when we build the perceptions, and Writes when the manager removes idle players. Hence here we have two different locks. |
|||
[[Marauroa]] |
|||
CREATE TABLE rpobject ( |
|||
id int(11) NOT NULL default '0', |
|||
slot_id int(11) default NULL, |
|||
PRIMARY KEY (id) |
|||
) TYPE=InnoDB; |
|||
Latest revision as of 02:49, 24 November 2010
Basic idea behind GameManager
The idea behind the Game Manager is to handle all the "business logic". This Manager decides how to reply to each individual message.
GameManager
The logic is similar to this:
GameManager
{
NetworkManager read Message
switch(Message type)
{
case ...;
}
}
So let's define the reply to each message. First, let's clarify that the best way of modelling this system is using finite automates, (a finite state machine) where, based on the input, we change the state we are currently in and produce an output.
Login stage
NOTE: This stage has been split in 3 to allow proper secure login. NOTE: Explain here how secure login works.
Process C2S Login ( STATE_BEGIN_LOGIN )
Precondition: The state MUST be NULL
Test if there is room for more players.
if there is no more room
{
reply S2C Login NACK( SERVER_FULL )
state = NULL
}
if check username, password in database is correct
{
create clientid
add PlayerEntry
notify database
reply S2C Login ACK
get characters list of the player
reply S2C CharacterList
state = STATE_LOGIN_COMPLETE
}
else
{
notify database
reply S2C Login NACK( LOGIN_INCORRECT )
state = NULL
}
Postcondition: The state MUST be NULL or STATE_LOGIN_COMPLETE
and a we have created a PlayerEntry for this player with a unique clientid.
Choose character stage
Process C2S ChooseCharacter ( STATE_LOGIN_COMPLETE )
Precondition: The state MUST be STATE_LOGIN_COMPLETE
if character exists in database
{
add character to Player's PlayerEntry
add character to game
reply S2C Choose Character ACK
state = STATE_GAME_BEGIN
}
else
{
reply S2C Choose Character NACK
state = STATE_LOGIN_COMPLETE
}
Postcondition: The state MUST be STATE_GAME_BEGIN and the PlayerStructure
should be completely filled or if the character choise was wrong the state is STATE_LOGIN_COMPLETE
Logout stage
Process C2S Logout ( STATE_GAME_END )
Precondition: The state can be anything but STATE_LOGIN_BEGIN
if( rpEngine allows player to logout )
{
reply S2C Logout ACK
state = NULL
store character in database
remove character from game
delete PlayerEntry
}
else
{
reply S2C Logout NACK
}
Postcondition: Either the same as the input state or the state currently in
Perception confirmation stage
Process C2S Perception ACK Precondition: The state must be STATE_LOGIN_BEGIN notify that the player received the perception. Postcondition: The state is STATE_LOGIN_BEGIN and Timestamp field in PlayerContainer is updated.
Transfer confirmation stage
Process C2S Transfer ACK
Precondition: The state must be STATE_LOGIN_BEGIN
foreach content waiting for this player
{
if client acked it
{
send content to client
}
}
Postcondition: The state is STATE_LOGIN_BEGIN and the content waiting for player is clear.
PlayerContainer Explained
PlayerContainer is the data structure that contains all of the information about the players while the game is running.
It consists of a list of RuntimePlayerEntry objects and is heavily linked with the PlayerDatabase, so we can hide the complexity to GameManager. By making PlayerDatabase hidden by PlayerContainer we achieve the illusion that managing the runtime behavior we modify automatically the permanent one.
RuntimePlayerEntry is the structure that contains the information about the player while it is online.
RuntimePlayerEntry contains:
- clientid
Clientid is the field that indexes players in the server. See the documentation about clientid generation to understand what they are and how they are generated.
- source
Source is the IPv4 address of the client. This is used to determine if the message is really coming from the client or another person trying to impersonate it.
- timestamp
Timestamp is used to determine if a client has timed out in which case it is only wasting resources on the server. As you may already know, UDP is not a delivery-guaranteed protocol, so we need to check for dead clients ourselves. Note that this only indicates that the player timed out and it doesn't apply any kind of measures on them.
- storedTimestamp
storeTimestamp is used to determine when the player was last stored in the database. We don't store each time the player info changes as this would obviously be very CPU time consuming. Instead we cached the changes and store them only every 5 minutes.
- username
Username is filled in at runtime with a Login event. If we store the username here we are able to use the database from PlayerContainer thus by knowing the clientid we can also now know the username without having to look to the actual database.
- choosenCharacter
choosenCharacter is filled in at runtime with a ChooseCharacter event. If we store the information here we are able to use the database from PlayerContainer and hence by knowing the clientid we also know the choosenCharacter without having to refer to the actual database.
- state
State is a number expressing the state in which the player is. There are four states:
- Have to login
- Login Complete
- Game begin
- Logout
When we create the entity, by default, the state is Have to login. Once you have logged in correctly, the state changes to Login Complete and once the player has chosen a Character it changes to game begin. The logout state is pretty trivial :)
The idea is that some operations are only allowed in certain states, so the state property stores which state they are in to make validating actions easier. ( To read about Perceptions, click here )
- perception counter
The Perception counter is used to keep an incremental count of the perceptions sent so that the client can see if it gets out of sync.
- perception Previous RPObject
Perception previous RPObject is the RPObject that was sent on the last perception. Using this we can track changes to a RPObject without disturbing the rest of the system.
- perception Out of Sync
This flag indicates to the server if the player has become out of sync. This allows us to re-sync it as soon as possible.
Hence, all we need to operate PlayerDatabase is a username and choosenCharacter. So using PlayerEntryContainer we can fully operate it.
ClientID generation
Each client MUST have a session id to prevent another player impersonating it. sessionid must be of short or int size to make guessing the ID much harder.
To make it even more secure, clientids are generated randomly for each player with the only condition that two different players MUST have two different clientids.
Synchronization between Game and RP Managers
Why bother with this? Well, imagine that a player logs out while the perception is being built, it will no longer be accessible by the RP Manager when it expects the object to be there, or if RPManager tries to remove a player which has already been removed, these situations are very serious as they will probably make the server fail.
So we must synchronize the Game and RP Managers.
The idea behind the solution is that the each manger requests access to the PlayerEntryContainer via a central mutex (a mutex is a syncronisation element attached to a resource, which can be owned by one task at any point in time. If the mutex is owned already when a task tries to access the object protected by it then the mutex will inform the task that it doesn't have access at this point in time to the object).
There are two types of accesses, read accesses and write accesses. Note that two readers can access an object in parallel but only one write can happen at the same time. In GameManager there are only Write actions, as this manager only modifies the state of the PlayerContainer. However, in the RP manager we have both Reads, when we build the perceptions, and Writes when the manager removes idle players. Hence here we have two different locks.