Most commands will need to read data from or save data to the database. Ares uses an off-the-shelf database tool called Redis. The Ohm database library lets you interact with the database directly from Ruby code.
Table of Contents
Ohm lets you define ruby classes that interact with the database. These are called Models, and you can identify them by the fact that they inherit from Ohm::Model
. Model classes define attributes that correspond to fields in the database. For example, the Character
class defines a name and alias:
class Character < Ohm::Model
attribute :name
attribute :alias
...
end
You’ve probably seen database fields used throughout the code, in examples like client.emit "Hello #{enactor.name}!"
In that case, enactor
is a character model, and name
is the database field.
You’ll need to look in the code to find out what database fields are available.
To create a custom database model, you just need to define a class that inherits from Ohm::Model
.
CORRECT
module AresMUSH
class MyThing < Ohm::Model
end
end
INCORRECT
module AresMUSH
module SomePlugin
class MyThing < Ohm::Model
end
end
end
Once you have your model class, you can set up your own attributes. For example:
module AresMUSH
class MyThing < Ohm::Model
attribute :name
attribute :some_field
end
end
The Game model is special because there’s only one game. You don’t really need to search for it. You can access the one and only game model using:
Game.master
The model class defines the data for all objects of that type. For example, the Character
model class defines the data for all characters and MailMessage
defines the data for all mail messages.
An instance is a specific occurrence of that model–a specific character, or a specific mail message.
You can create an instance of a model using ClassName.create
, and optionally pass attribute/field data. For example:
char = Character.create(name: "Jack")
The variable char
now refers to your new database object instance, so you can use that instance to read and update fields, as described in the next section.
Deleting an object is as simple as:
char.delete
Read on for more information about finding and updating objects.
All Ohm models have an implicit id
field that identifies a specific instance. This is just an auto-incrementing integer.
char = Character.find_one_by_name("Bob")
client.emit "Their ID is #{char.id}!"
Unlike in older MUSH codebases, IDs are per model type. Character 9 is a completely different object than Room 9.
Once you have a database object instance, you can use its properties (aka fields or attributes) as simple methods.
Fields are defined using the attribute
keyword on the model class. For example, the Character class has a name
field:
class Character < Ohm::Model
attribute :name
...
end
You can read properties just by using the attribute name like a method. For example:
char = Character.find_one_by_name("Bob")
client.emit "Hello #{char.name}!"
You can update the properties on a database object instance using the update
method. For example:
char = Character.find_one_by_name("Bob")
char.update(name: "Harry")
Many database properties have specialized update methods because their data storage is not so straightforward. Look around for helper methods in the plugin module:
char = Character.find_one_by_name("Bob")
Demographics.set_group(char, "Faction", "Navy")
char.name = "Harry"
) This changes the property on the ruby object in memory but does not actually update the database unless you call char.save
at the end to commit the changes to the database.
Under the hood, all redis fields are stored as strings. The Ohm database library provides some utilities to let you tell the Ruby code to interpret certain fields as different data types, as shown below:
attribute :team, :type => DataType::Integer
attribute :freshly_damaged, :type => DataType::Boolean
attribute :armor_specials, :type => DataType::Array, :default => []
attribute :prior_ammo, :type => DataType::Hash, :default => {}
attribute :starts, :type => DataType::Time
attribute :birthdate, :type => DataType::Date
The Ohm database library provides a variety of ways to query for database models, but here are a few of the easiest:
Character[id]
- Finds an object directly by ID.Character.find_one_by_name(name)
- Finds the first character with that name/alias.Character.find_any_by_name(name)
- Finds all characters with that name/alias.Character.all
- Finds all characters.If your database isn’t gigantic, you can also use the Ruby select
statement to filter objects based on any set of criteria. If your database is huge, you’re better off using Ohm’s queries. They’re a little more complex, but faster. Here’s an example that will find all characters with the ‘admin’ role:
def handle
chars = Character.all.select { |c| c.has_any_role?("admin") }
names = chars.map { |c| c.name }
client.emit "You found #{names.join(", ")}"
end
select
and map
together like this. Select will find a bunch of database objects, and map will get just the field you need from them (in this case the name).
There are a few convenient utilities to help with common searches. All of these will search by name or (for players) by alias, and include the me
or here
keywords, so they require you to pass in the character doing the search.
ClassTargetFinder
- Searches for a single object of the specified type.AnyTargetFinder
- Searches for a single object among rooms, characters or exits. Helpful for commands like describe/destroy that can work on multiple object types.OnlineCharFinder
- Searches for a single matching online character.VisibleTargetFinder
- Searches for a matching object in the same room - either a character, room or exit.All of the finder helpers do the error handling necessary to translate multiple matches into a message like “I don’t know which one you mean” and no matches into “I don’t see that here”. They return a FindResult
object containing a ‘found’ indication and either the target object or an error message.
result = ClassTargetFinder.find(name, Character, searcher)
if (result.found?)
# Object was found - do something with result.target
else
# Object was not found - do something with result.error
end
Several of the more commonly-used target finders have another level of helper utility you can use. Here’s how it works:
ClassTargetFinder.with_a_character(name, client, enactor) do |model|
# Do something with model if it's found
end
with_a_character
will do the stuff in-between the do
and the end
if the character was found, using the variable model
for the character it found. If the character was not found, it will emit the appropriate error message to the client.
There’s a similar helpers for with_something_visible
and with_online_chars
.
Some database models use Callbacks - methods that are triggered in response to certain database events. There are two possible callbacks in Ares, called before an object is deleted or before it’s saved:
before_delete
- If an object has any references, you may want to delete them before the object itself is deleted. For example, a room might delete all of its exits when it’s getting deleted.before_save
- If an object has special fields, you may set them before the object is saved. For example, if you store the uppercase version of the object name for fast lookups, you want to update that to match the object name every time the object is saved.Although Redis is not a relational database like SQL, we still need to define relationships between our objects. A mail message has an author and recipients (all characters), a scene is attached to a room, etc. Ohm provides some built in concepts that let us manage these relationships. See Database Relationships.