Multiplayer Interactive-Fiction Game-Design Blog


Documentation – Server Library



Download 8.87 Mb.
Page105/151
Date02.02.2017
Size8.87 Mb.
#15199
1   ...   101   102   103   104   105   106   107   108   ...   151

Documentation – Server Library

01 Database Overview


The database functions (DatabaseXXX) can be used to store large numbers of objects to disk.


Database functions
The Circumreality server provides a set of database functions you can

use to save objects (from your virtual machine) onto disk

when they're not used. This saves memory and processing power.

For IF, you can use the database if your world is too large

to fit in memory, or you have objects that aren't needed

all the time. For example: You may wish to save information

about your players and their characters in separate databases,

only loading in the information when the player has logged on.

When the player logs off, the information is saved back to disk.

The database is also useful because it saves to disk, which

is good protection in case the server crashes. You can even

have the database make daily backups to other directories

on disk, or even other servers connected through a LAN.

Database categories
The first issue you need to tackle when dealing with the

database functions is to determine what database "categories"

you'll need. Each category represents a separate list of

objects stored in its own file.


For IF, you'll probably want the following database categories:

"Players", "PlayerCharacters", "SaveToDatabase", and "Objects".

You may also want categories for "EMails", and "NewsgroupPosts"

so that users can send message to one another.

Database categories are automatically created when the category

is referenced. You don't need to call any funtions.
Cached properties
Each database category stores a list of objects. Each

stored object retains its class and properties.

When you wish to retrieve one or more items from a database

you'll need to search the database (do a query) for objects

whose properties match certain qualifications.

For example: When a user types in their player name, your

application will query the "Players" database for all objects

whose "Name" matches the name typed in by the user.

Likewise, when the user wishes to select a character, you'll

need to query the "PlayerCharacters" database for all objects

whose "Player" matches the player name.

To make queries as fast as possible, the database "caches"

one or more properties from the objects. The cached properties

are usually those used for queries. While you could conceivably

cache every property in all the objects, you shouldn't because

that would make the cache information very large, defeating

the memory-saving ability of the database.

For example: From the "Players" database you would definitely

cache "Name". You might also cache some properties useful for

administration, like "TotalTimeLoggedOn". The "PlayerCharacters"

database would cache "Name" and "Player". You might also

cache "Level", "Race", "Class", etc. so you can quickly generate a list

of all player characters along with their race and level.

To add a property to be cached call DatabasePropertyAdd() with

the database name and cached property as parameters.

(Example: DatabasePropertyAdd ("Players", "Name");)

Repeated calls add more properties. (If a property is already

on the cached list then the call will just return TRUE

for success, since the property is already cached.)

One important thing to remember about cached properties is that

if a property is added AFTER the database has objects, whenever

that property it accessed for the pre-existing objects it will

return Undefined. As the objects are checked out (accessed), however,

the cache list will be updated with their correct values.

What this means is that you should define what properties

are to be cached before your databases are finalized.



Adding an object to the database
To add an object to the database:


  1. Create it as normal, using "MyObject = new cMyClass" (or whatever).





  2. Call "DatabaseObjectAdd ();" with the database name and the

    object. Example: "DatabaseObjectAdd ("Players", MyObject);



At this point the object will be saved to the database and

immediately "checked out". An object that is checked out

can only be accessed by the application that has it checked

out (preventing another application from using the same player).

Furthermore, an object that is checked out must be

"checked in" for the changes to be saved.

Once an object is added, use it as normal until your

program is ready to delete the object. At that point

the program needs to check in the object.


Checking in an object

You can think of the whole process of checking out and checking

in an object like a visit to the library... to find a book

you first visit the card catalog (the database query). Once

you know what book you want, you get it from the shelves and

check it out from the librarian. From then on, only you have

access to it. When you're finished with it, check it back in,

and it goes back on the shelves and is available for other

visitors.

When your application is finished with a checked-out object,

it should call "DatabaseObjectCheckIn()", passing in the

database category and the object. This will save the object

to disk and DELETE it from memory (just like

calling "delete MyObject"). If you don't want the object

deleted from memory then you can pass a parameter into

DatabaseObjectCheckIn() that will cancel the deletion.


If your object contains other objects (such as a player character

holding possessions) then you MUST check in the contained

objects first before checking in the container object.

Conversely, when you check out, your MUST check out the container

first, followed by the contained objects. If you do check out/in

in the wrong order MIFL will not properly remember which object

is contained within which other object.


If you merely want to save your object to disk without checking

it in, call "DatabaseObjectSave()" instead. You may wish to

do this for two reasons: 1) If the computer crashes, all the

information about an object will be lost unless it's saved (or checked

in). 2) When an object is saved, any properties from the

will be updated in the cache, allowing for more up-to-date queries.

Checking out an object
You can check an object out of the database by

calling "DatabaseObjectCheckOut()", using the database

category name and the object.

Checking an object out will load it off disk and create

a version of the object in memory. (In many ways, the

functionality is similar to a call to "new cMyClass" except

that the newly created object has all the properties and

timers of the checked in class.)


Once an object is checked out you can use it as normal.

When you're finished with it, check it back in.

To check an object out, however, you need to know which

object you want. This involves a trip to the card catalog (aka:

a database query.)


Database queries
To get a list of all the objects in the players database, just

call: DatabaseQuery ("Players", NULL, '|');

This will return a list of all the objects in the database.

You can then use DatabaseObjectCheckOut() to get each

object in turn, examinine it, and check it back in.

If you wished to find the player object belonging to the

player that's logging in, checking out and then checking in

every single player object in the database would be very

slow.

There is a better way to do this: Call

"DatabaseQuery ("Players", ["==c", "|Name", "Tim Jones"], '|');

instead. This call will return a list of objects whose

name (property) is "Time Jones".

Here is an explanation of the paramters:





  1. The "Players" entry informs the query to look in the "Players"

    database.



  2. "["==c", "|Name", "Tim Jones"]" tells the query to find

    all objects whose "Name" (property) is equal ("==c") to "Tim Jones".

    The '|' in front of "Name" identifies it as a properties, as

    opposed to a string. ("Tim Jones" does NOT have a '|' in front of it.)

    The "==c" is a case-insensative comparison.





  3. The '|' parameter identifies what character is used to indicate

    that a string is actually a property name, as opposed to a string

    to compare against.





The "==c" is called an operator. It specifies what kind of

comparison to use when comparing the "Name" property to "Tim Jones".

There are a couple dozen different operators (see the

DatabaseQuery() documentation). For example: "!=c" would find

all players whose name is NOT "Time Jones", while "containsc" would

find all players whose name contains the string, "Tim Jones", so

"Tim Jones III" and "Hittim Jones" would be returned from the

query.

You can also compare several properties (as explained in

the DatabaseQuery() documentation). For example: To find

all players named "Tim Jones" or whose name contains "Tim", pass

in "["||", ["==c", "|Name", "Tim Jones"], ["containsc", "|Name", "Tim"]]".

The "||" is a logical for the next two operands.



Avoiding checking out and checking in
Sometimes your application will just need to get a few

properties from an object (which may be cached) and doesn't

really need to check out the object.

For example: You may have done a query for all the

objects whose name contains "Tim" and now want to display

their names and ages on the screen. You could go through

each object and check it out, get the property values, and

then check the object back in. This would be slow.

It also has a problem: If the player object was checked out

elsewhere you wouldn't be able to access it.


You could also call "DatabasePropertyGet()" to get the

properties directly.

Example:




  1. Call "ListOfTims = DatabaseQuery ("Players", ["containsc", "|Name",

    "Tim"], '|');" to fill "ListOfTims" with a list of all players

    named Tim.





  2. Call "ToDisplay = DatabasePropertyGet ("Players", ListOfTims, "Name");"

    to get the "Name" property from all the objects in "List of tims".

    This might return ["Tim Jones", "Tim Jones III", "Hittim"].





If you wished to get both the "Name" and "Age" properties from

the players in the list, you could always call DatabasePropertyGet()

twice. Or, you wrap the property names into a list

and call: "DatabasePropertyGet ("Players", ListOfTims,

["Name", "Age"]);" This will return a list of lists. The

first level of the list will correspond to all the objects.

The second level (lists within the main list) will be

the values for the "Name" and "Age" properties respectively.

If you just wished the name for the first Tim, calling

"DatabasePropertyGet ("Players", ListOfTims[0], "Name");" would

return "Tim Jones". (Notice the [0] index onto ListOfTims.)


Be aware that while DatabasePropertyGet() will get properties

for objects that are checked out, the property value may not

be up-to-date. If an object is checked out and its "Name"

property change, calls to DatabasePropertyGet() will return

the old name. At least until the object is checked in, or

the object is saved.

Avoiding checking out and checking in, part 2
You can use a sister function, "DatabasePropertySet()" to change

one or more properties without actually checking out (and then

checking in) the object.

NOTE: DatabasePropertySet() will NOT be able to modify an

object that is checked out. It can only modify objects

that are checked in. (DatabasePropertyGet() can access properties

from objects that are checked out or in.)

Example: To change a player's name without checking it out,

call "DatabasePropertySet ("Players", ListOfTims[0], "Name",

"Tim's new name");"


Just as DatabasePropertyGet() can accept a list of objects

to get (as opposed to just one object), or a list of

properties to get (as opposed to just one property), so

to can DatabasePropertySet(). For more information see

the DatabasePropertySet() documentation.


Deleting checked-out objects
If you delete a checked-out object using

"delete XXX", or "XXX.DeleteWithContents()", or

"DeleteGroup()", then if the object is checked out, it

will automatically be deleted from the database. If

it is not checked out, then its database

entry will be unaffected.




02 Saved Games


The SavedGameXXX() functions can be used to save a game, or group of objects.


Saved games

You can use the SavedGameXXX() functions to load or save

a game (for a single player game), or to create instanced

areas of the game.


To save a game:





  1. If you have values of global variables that

    you wish to save, you need to put the global variables

    into a named object. Or better yet, design your IF title

    so it doesn't use global variables to store information

    that might change across game saves.







  2. Determine every object that needs to be

    saved. You don't need

    to worry about child objects since they can be handled

    automatically.




  3. If most of the objects need to be saved (such as for saving

    an entire game), then determine which objects don't

    need to be saved. For example: You won't wish to

    save the connection objects because if you do, reloading the

    saved game will lose connection information.

  4. Call SavedGameSave(), passing in a game

    name and the list of objects you wish

    to save (or exclude from saving). The function accepts

    a flag indicating whether or not you wish to save all objects

    and exclude from the list, or save only in the list.

    It also accepts a flag which causes all the children (and

    their children, recursive) to be saved.





That's it. To reload the game:





  1. Call SavedGameLoad() with the name of the game

    and a flag indicating if you wish to remap objects.


  2. If remapping is set to TRUE then any objects you load that

    already exist will be given new IDs (and the old ones will

    be kept around).

    If remapping is FALSE then loaded objects

    will replace any old ones.

    If the game was saved with the SaveAll flag

    set to TRUE then any deleted objects will also be deleted.

You can also call SavedGameEnum() to list

saved games, or SavedGameRemove() to delete

saved games.



Advanced saved games

When you save a game, you need to specify both a file name, and

a sub-file name.

The file name corresponds to an actual

file written to disk, in a sub-directory of where the databases are stored.

For example: The "MySaves" filename would be translated

into "[DatabaseDir]\SavedGames\MySaves.msg".

The sub-file is written within the main .msg file.


The advantage of this system is that instances (see below) for players

on a multiuser game can be easily saved. Each character can be assigned

a main file name (probably based on their character's object ID). All

of the instanced maps are saved as sub-files within the character's

save-game file.



Creating an instanced dungeon


NOTE: Instances are handled by the Basic Interactive Fiction

library. If you want to use instances, you should use the functions and

methods provided there. However, you may find this section interesting

because (a) it explains the basic principles, and (b) it lets you rewrite

the code in the Basic Interactive Fiction library.

An instanced dungeon (or area of the world) is a copy of the

world that can only be accessed by select PCs (usually friends).

A section of the world may be instanced several times over

so that many groups can play in multiple copies of the same area.

To create an instanced dungeon:







  1. Build the section of your world that will

    be instanced, and note what rooms are in it. Keep

    the list of rooms in a global.



  2. Make sure there are no direct entrances into

    the instanced area so that players and monsters cannot

    just walk in.





  3. Provide a door with custom code that will redirect

    each player to the right instance of the world. When the

    PC walks through the door, it figures out which instanced

    world should be used and calls ActorMove() to move the player

    to that world.





  4. The rooms should always be kept sterile, which means no

    player characters or NPCs from outside can enter, and nothing from within

    can leave. They reason they're sterile is because they act as a template

    for all the instances.



  5. When a player enters an instanced set of rooms, and the rooms need to be

    created, call SavedGameEnum() to see if the player already

    has entered the instanced rooms.




  6. If this is the first time the player is entering the instance,

    call ObjectClone() to clone all the rooms and their

    contents. The player will be allowed to enter the copies of the

    cloned rooms.

    If the player has entered, the call SavedGameLoad() to load

    in the instanced rooms. This will loaded in the instanced rooms that,

    at some point, were created using ObjectClone().

  7. When a player needs to be moved into the instance, find

    the room to move them into using the remapping list,

    searching for the old room and returning the new one.





  8. That's all you need to do, except when all the players leave

    the instanced dungeon and are finished with it,

    call SavedGameSave() and delete all of the

    instanced rooms and their contents.





03 Online Help


Describes the online help system.


Online help

Creating online help for a Circumreality title is easy. All you need

to do is create a number of "Help" resources; they will

automagically be placed into a table of contents and indexed.


To create a help resource:





  1. Select the Add new resource option from

    the Misc menu,



  2. In the "Add a resource" page that appears,

    press the "Help" button.



  3. In the "Modify resource" page, type in

    the help topic name, like rHelpGetCommand. The

    resource name should be unique for each help topic.





  4. Press "Add" to add a resource for a specific

    language.



  5. In the "Help resource" page that appears, type

    in a Name that will be visible to the players.

    This name must also be unqiue for each help topic.





  6. Type in the Category in which the help topic

    will be placed (for the table of contents). To place

    the help topic at the top of the TOC, leave this blank.




  7. The TOC is like a directory tree. If you wish to place

    the help topic several sub-directories down, just use a

    forward slash ('/') to separate them. For example:

    "Plants/Trees/Evergreen", will create a "Plants" category

    at the top of the TOC. If the user clicks on that they

    will see the "Trees" category. Underneath that is the

    "Evergreen" category, and that contains the article

    for pine trees.


  8. You can place the article in a secondary category if

    you wish, or leave the entry blank.



  9. Enter a short description for the category like,

    "An article about pine trees".



  10. Next you need to enter the MML text for the category.

    MML is a lot like HTML (used for web pages). If you have

    never used HTML before, look up some reference manuals

    on the Internet, or look at some other help topics to see

    how the format works.




  11. Press the Test MML button to make sure you don't

    have any mistakes in your MML, and that it looks good.


Those are the basics. When you next run your Circumreality title the

help article will automatically be added to the table of contents

and indexed.





Advanced online help

At the bottom of the help-resource page are some options

that might come in useful:



  • The Function and Function parameter fields

    are useful for any help topics that should only be seen

    by administrators or characters with specific skills.




  • If you fill in "Function" with a function name, the function

    will be called if the user tries to access the help topic.

    The first parameter for the function will be the actor searching

    for help, and the second will be the "function parameter" (as

    a string.) If your function returns TRUE, the player will be

    allowed to see the help topic. Returning FALSE will hide the

    help topic from the player.

  • The Book field allows you to divide help into

    several books, and only show topics for a specific book.

    While this functionality is not usually needed, it does come

    in handy for role-playing knowledge.


  • For example: You could

    write several help topics about Elvish culture that the player

    would only have access to if they played an Elf character (or

    learned some sort of skill). Then, you could create a new

    command that mimiced the "Help" command, but was designed

    for role-playing knowledge, like a "Do I know" command.

    If the user had Elvish role-playing knowledge they would

    be given access to the help in the "Elvish" book.






Online help functions

Some useful online help functions are:







  • HelpArticle() searches for a specific help

    article and returns the MML resource (that can be sent to

    the player's computer), along with the categories the

    help topic is in.







  • HelpArticleMML() just returns the MML

    resource. It automatically includes links at the end of the

    page so the player can see the TOC where the article is

    placed.






  • HelpContents() returns a list of all the

    articles and sub-directories given a directory in the

    table of contents.





  • HelpContentsMML() returns a MML resource

    with all the contents information from HelpContents().







  • HelpSearch() performs a keyword search.





  • HelpSearchMML() returns a MML resource from

    a keyword search.







04 Automatically downloaded data and files


A little bit of information about automatically downloaded data and files.


Automatically downloaded data and files

When Circumreality produces a .crf file, it automatically scans all the

resources for file references, such as .wav files, and includes them

in the .crf file. That way, any data file referenced in a resource

will be included in the .crf file for distribution.

If the IF title is run multiuser over the Internet, then any

file in the .crf file will be transferred over as its needed.

There are some exceptions to this... the Circumreality client's install

will install a few files that are commonly used in all Circumreality

titles. These include MikeRozak.tts (text-to-speech

voice), EnglishInstalled.mlx (lexicon for text-to-speech),

and LibraryInstalled.me3 (basic 3d objects).

The Circumreality client will not download these files from

the server.


Furthermore, if a 3rd party application copies a .tts or

or .mlx file into the client application's directory then it

will use those rather than installing them.


What this means:





  • If you have a more recent (or older) version of the .tts or

    .mlx files listed, you cannot guarantee that your versions

    will be used. (The LibraryInstalled.me3 won't be any problem

    though; it will update properly.) Usually, this is not

    a problem.





  • The user may have installed a high-quality TTS voice over

    the default voices, resulting in better sounding TTS.

    A high quality TTS voice is 50+ megabytes, and not something

    most people want to download.



See also the "Binary database", since files in the binary database

are automatically downloaded.

05 Binary database


Shows how to use the binary database to store images that players upload.


Binary database

Players may wish to upload images of their characters so that whenever

another player sees their character, the image will automatically

be downloaded. Likewise, sounds could be customized by the players.


The binary database lets you accomplish this. The database

just stores binary data accessed by a file name, such as "test file.jpg".

Various functions are provided to add/remove data to the database,

see below. Once data is in the database, any automatic download

requests from the client will access the database, as well

as precompiled binary information. Thus, any reference to "test file.jpg"

on the client will automatically pull the data from the binary database.


The binary database supports the following methods:







  • BinaryDatabaseEnum() - Enumerates all entries in the

    database, or all entries beginning with a specific prefix.







  • BinaryDatabaseGetNum() - Gets the name of a database

    entry based on the index into the database.







  • BinaryDatabaseLoads() - Loads binary data from the

    database.







  • BinaryDatabaseNum() - Returns the number of entries

    in the database.







  • BinaryDatabaseQuery() - Returns the size and modification

    dates for the data.







  • BinaryDatabaseRefresh() - Sends a message to all the connected

    clients that a file in the database has been changed, and that they should

    reload it.





  • BinaryDatabaseRemove() - Deletes an entry from the binary

    database.







  • BinaryDatabaseRename() - Renames a database entry.





  • BinaryDatabaseSave() - Saves binary data into the database.





06 Text/event logging


Describes how to log text into the text log.


Text/event logging

Circumreality has a "text log" that can be used to store gigabytes of

log information, such as when users log on, log off, what actions

they take, etc. Logging is vitally important for finding bugs as

well as detecting players that cheat.


Log everything

As a general rule, log everything, or at least, provide the ability

to log everything.

Categorize each event into one of four levels:







  • Very important (Category 1) - These are events that you

    always want to log, such as when your code realizes that its data

    is corrupt, or even that a user has logged on/off.





  • Important (Category 2) - Category 2 events will always

    be logged too, but they're not as critical. For example: Log commands

    that the user types.





  • Nice to know (Category 3) - Category 3 events are only logged

    if you are suspicious about a specific user, or you set the system

    to an elevated state of alert. For example: A character picks up

    or drops an item.







  • Unimportant (Category 4) - These events are only logged

    if a user is marked as being very suspicious or the system is set

    to a very high state of alert. For example: Storing all the MML sent

    from the server to the clients.





In you code, you should check

either gTextLogUser[EVENTLEVEL] or gTextLogSystem[EVENTLEVEL] before

logging an event. If the value is TRUE then log the event.

Use gTextLogUser[] if the event pertains to an action from the specific

user. gTextLogSystem[] is for events that are independent of the user,

such as something a NPC does.

There are three ways to log an event:







  • TextLog() - This logs a string, as well as an optional object.

    The current user, the user's character, and the location of the user's character

    are also logged. You will probably use TextLog() more often than the other

    two logging functions.







  • TextLogNoAuto() - Like TextLog(), this logs a string, but

    it lets you specify which user object, character object, room object,

    and object are associated with the log. Use TextLogNoAuto() if, for example,

    a NPC acts.







  • Trace() - Calling Trace() will only log the text line. You

    won't be able to store additional information in the log, such as

    the user or room.



Combining, these two together, a sample line of logging code might

look like:




if (gTextLogUser[3])


&tab;TextLog ("Pick up", vObjectThatPickedUp);





Some more details

The globals, gTextLogUser and gTextLogSystem, are intializes by

calls to TextLogPriorityAdjust(). TextLogPriorityAdjust() is

called in the connection object when a message arrived from a user.

It does the following:



  1. Adjusts gTextLogUser and gTextLogSystem. These

    values are affected by gTextLogAlert, which controls the

    overall system alert, and ther user's pUserLogAlert which

    controls what priority messages are to be logged for a specific user.

    Increase gTextLogAlert to record lower priority (higher value) events

    for all users. Increase pUserLogAlert to record lower priority (higher

    value) events for a specific user, such as one that might be cheating.





  2. The function also calls TextLogAutoSet() to tell

    the text log what user, room, and player character to automatically

    use when TextLog() is called.





It is unlikely that you will need to call TextLogPriorityAdjust() directly,

but knowing of its existence helps you understand what's going on.



Accessing the text log

Circumreality provides several functions that are useful for accessing the

text log database:





  • TextLogAutoGet() and TextLogAutoSet() - Let you set the

    user, character, and room that are used by TextLog().







  • TextLogDelete() - Deletes one of the text logs files.

    Circumreality creates one new file every hour. Circumreality includes code that automatically

    deletes logs older than a few days, affected

    by gTextLogDeleteOld.







  • TextLogEnableGet() and TextLogEnableSet() - Completely disable

    text logging. These are used when Circumreality is running in an offline, single-player

    mode, and logging isn't warranted.





  • TextLogEnum() - Lists all of the log files, one per

    hour that the world has been running.







  • TextLogNumLines() - Returns the number of events logged

    in specific text log file.







  • TextLogRead() - Reads in a line/event from a specific

    text log file.







  • TextLogSearch() - Searches through the text log files

    for all events that occur within a specific date/time range, for

    a specific user, character, room, or object, and with a specific

    sub-string.







Download 8.87 Mb.

Share with your friends:
1   ...   101   102   103   104   105   106   107   108   ...   151




The database is protected by copyright ©ininet.org 2024
send message

    Main page