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:
-
Create it as normal, using "MyObject = new cMyClass" (or whatever).
-
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:
-
The "Players" entry informs the query to look in the "Players"
database.
-
"["==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.
-
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:
-
Call "ListOfTims = DatabaseQuery ("Players", ["containsc", "|Name",
"Tim"], '|');" to fill "ListOfTims" with a list of all players
named Tim.
-
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.
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:
-
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.
-
Determine every object that needs to be
saved. You don't need
to worry about child objects since they can be handled
automatically.
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.
-
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:
-
Call SavedGameLoad() with the name of the game
and a flag indicating if you wish to remap objects.
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.
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:
-
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.
-
Make sure there are no direct entrances into
the instanced area so that players and monsters cannot
just walk in.
-
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.
-
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.
-
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.
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().
-
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.
-
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.
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:
-
Select the Add new resource option from
the Misc menu,
-
In the "Add a resource" page that appears,
press the "Help" button.
-
In the "Modify resource" page, type in
the help topic name, like rHelpGetCommand. The
resource name should be unique for each help topic.
-
Press "Add" to add a resource for a specific
language.
-
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.
-
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.
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.
-
You can place the article in a secondary category if
you wish, or leave the entry blank.
-
Enter a short description for the category like,
"An article about pine trees".
-
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.
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.
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.
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.
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.
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.
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.
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);
|
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:
-
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.
-
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.
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.
Share with your friends: |