Abstract

This document contains the basic agreement used to implement a Data Model in Dime.

ATTENTION: Though you can in general rely on the interface specification now, minor details could be changed (and probably will). Thus apply good documenting on your code, so that you can understand it yet some days later. ;-)

TODOs:

Version History

Version Date Author Nick Changes
0.1 2002-02-03 Tim Enderling Tim Initial version.
0.3 2002-02-09 Tim Enderling Tim Reviewed version after discussion with zzorn on #dime.
0.4 2002-04-04 Tim Enderling Tim Last? review before actual implementation

General idea of a Data Model

A Data Model like used in Dime is the homogenous collection of most if not all available data of the application. This is considered as very useful in connection with scripting and debugging or - in general - to make data more accessible and managable. I.e. it allows us to support different games or parts of a game, that expose different kinds of interfaces, without having to hard code an user interface for the different new features and recompile. This allows much more dynamic control of the avatar and game. E.g. when controlling a car or airship, custom UIs can be shown.

Of course, this approach has been taken by many developers, before: Take e.g. the proc filesystem of Linux or the IDispatch interface of the Component Object Model especially used in M$ VB. Thus it is not necessary to convince anybody how many advantages can grow of such a framework.

In theory the Data Model is a tree of Data Objects, thus also called nodes, labeled with an ID. Each ID can be an item of a path that connects a node of a higher level (esp. the root level) to a node that can be find by only going from higher to lower levels, i.e. in depth direction. Such a path is a string consisting of the ID's of the nodes it is going through, separated by slashes ('/'). The IDs are case sensitive and have to be unique within all IDs of direct childs of the parent node (and cannot contain slashes themself, of course). In addition they have to be constant, as long as the Data Object exists.

You cannot rely on a special order of child nodes. If this is needed sometime, it has to be implemented by giving an order-function for the ID's of the child nodes.

Another important aspect of the Data Model is it's virtual nature. There will be no (more realistic: little) extra memory needed to hold the Data Objects of the Data Model, because the Data Model tree is just a mapping of the real data in the programm to a more convenient form. So you also can generate a value of a Data Object everytime you need.

One of the advantages in convenience besides scripting is the possibility to connect a Data Object to a signal handler, called whenever a Data Object is changing. This connection mechanism can easily be generalized and implemented because of the homogenous structure of the Data Model in constrast to the heterogenous structure of the real data. Signaling is done by SigC++ library.

Types of Data Objects

There are three overall types of Data Objects:

Since we want to get a homogenous tree of Data Objects, all differences arising from types are of generous nature. Thus you can also recombine them to new types if you want. There will be no different interfaces for folders, streams, integers, strings or links.

How should the interface look?

Because the Data Model itself is just a mapping to the real data, the convience of the interface and it's implementation are the main goals to achieve. In a view to the use of the Data Model there are three parties to consider:

Events and Signals in the Data Model

The terms event and signal are synonymously used within this document and denote actions that change of any detail, i.e. structure/value, of the Data Model. Observing of events is done by libsigc++. There are two possibilities to observe such events:

Possible events are:

C++-identifieris fired...
PRE_VALUE_CHANGE... before the value is changed. (So Data Model Users have the possibility to retrieve the old value.)
POST_VALUE_CHANGE... after the value has changed.
POST_CHILD_ADDITION... after a direct child, which can also be a link, was added.
PRE_CHILD_DELETION... before a direct child object, which also can be a link, is removed.
PRE_DELETION... before a Data Object will be deleted.

Since not every observer wants to handle all types of events, you can OR them together to get a combination of interesting flags. Futhermore not all Data Model Providers can support all types of events. Which events are fired for a specific Data Object is included in the type information available for all Data Objects. Data Objects not sending PRE_DELETION cannot be used as destination of links.

Type information

The type information of a Data Object is a combination of the following flags (I don't think I have to mention that some flags exclude others... Oops, so I've done it though.)

C++-IdentifierWhat it means, if the flag is set
(event types, see above)The Data Model Provider fires this type of event.
VOID (is pointer to NULL), FOLDER, LIST, STREAM, BOOLEAN, INTEGER, FLOAT, STRING, CUSTOM_TYPE That a Data Object is of a certain type. This information is not - as one can assume - necessary to use the Data Object. Instead all modes of usage are described separately below. But for information (as for debugging purposes) it should be really useful.
HAS_CHILDSThe Data Object can have child nodes in generell.
ADOPTS_CHILDSThe Data Object can have child nodes owned by other Data Model Providers. This also means that you can links as child objects.
CAN_SET_VALUEThe Data Model Users can change it's values.
CAN_ADD_CHILDSThe Data Model Users can add childs.
CAN_REMOVE_CHILDSThe Data Model Users can remove childs.
IS_LINKThe Data Object is actually a link. Though it is not necessary - due to the implementational approach of links - to know when you treat a link and when an actual data object, this is considered as useful for debugging purposes.

Data Model Providers should always be polite in cases of invalid calls to their interface functions by at least ignoring them. The only exception of this is, that a Data Model Provider can be sure that it the getting and setting functions are only called for the right type.

The Data Object interface

In accordance to this analysis, we're going to have a basic interface called DataObject, providing all access facilities to the Data Model User, needed. It's implementation is part of the Data Model Service.

class DataObject
{
public:
     /**
     * returns the real ID of the node. (In cases it is a link this is different from the
     * destination of the link.)
     */
    std::string getID();

    /**
     * returns the destination of the link.
     */
    DataObject * getLinkDest();

    /**
     * returns the real path to the node from the root node.
     */
    std::string getPath();

    /**
     * returns the type of the Data Object consisting of different flags described above
     */
    Type getType();

    /**
     * returns the description of a Data Object.
     */
    std::string getDescription();

    /**
     * All of the following value getting and setting methods can also be applied
     * to Data Objects of other types. In this case the values are converted in some default way. Some
     * of the conversions don't work for the setting method.
     *
     * If the value of a Data Object is 0 or "" or it has no child nodes, it's boolean value is FALSE,
     * otherwise TRUE.
     *
     * Booleans are presented as -1 for TRUE and 0 for FALSE, if asked for integers. Floats
     * are rounded to get an integer and strings are parsed (some kind of atoi). Folders return the
     * number of child nodes as integer.
     *
     * Float convertions are performed in the same way as integer conversions.
     *
     * String conversion of boolean values are "TRUE" and "FALSE". Other string values are
     * some printf-like version of integers and floats and the ID of a folder.
     */

    bool        getBoolVal();
    int         getIntVal();
    float       getFloatVal();
    std::string getStringVal();
    void        setBoolVal(bool newValue);
    void        setIntVal(int newValue);
    void        setFloatVal(float newValue);
    void        setStringVal(std::string newValue);

    /**
     * returns a direct/indirect child specified by the subpath.
     */
    DataObject * getChild(std::string subpath);

    /**
     * returns an returns an iterator through child list, that is usable like an STL iterator.
     */
    DataObjectIterator getChildsBegin();

    /**
     * returns an returns an iterator pointing after the last child.
     */
    DataObjectIterator getChildsEnd();

    /**
     * returns a new Data Object added as child node. I linkSource is specified, a link to
     * a linkSource is added as new child Data Object.
     */
    DataObject * addChild(std::string newID, DataObject * linkSource);

    /**
     * same as above, but generates an ID automatically. This is useful for lists, where you don't
     * need an ID.
     */
    DataObject * addChild(DataObject * linkSource);

    /**
     * Adds a new connection to the signal.
     * If fireOnChildChange is true, the signal is fired whenever a change anywhere in the branch was
     * detected. The supported types of events part of changeTypes are returned. So if this
     * function returns changeTypes all wanted types are supported. 0 if none of them is supported.
     */
    Type addConnection(ChangeSlot & slot, Type changeTypes, bool fireOnChildChange);

    /**
     * Removes a connection added before.
     */
    void removeConnection(ChangeSlot & slot);
};

Data Model provider interface

The Data Model provider interface has to be as easy to implement, as the DataObject interface is to use. Where it gets difficult you can mostly use predefined implementors of the DataModelProvider interface.

Attention: Whenever referring to the term subpath, a string constisting of '/'-separated substrings is meant, which is NOT beginning with a '/' and not ending with one.

class DataKey
{
public:
    DataKey() {}
    virtual ~DataKey () {}

    /**
     * This class is what is used in OOP instead of  'void * cookie'
     */
};

class DataModelProvider
{
public:
     /**
      * Returns the ManageData pointer every DataModelProvider has to hold for the DataModelService.
      */
     virtual ManageData * getManageData() = 0;

     /**
      * Returns a new DataKey to an arbitrary child of a Data Object given by its DataKey. As
      * the ID of a child node, DataKey's cannot change till objects deletion.
      * NULL is used for the root node of the hosted branch.
      * The return value is NULL if the ID is unknown or another Provider is responsible.
      * In this case provider is filled with the responsible provider.
      */
     virtual DataKey * getChild(DataKey * parent, std::string ID, DataModelProvider *& provider) = 0;

     /**
      * Returns the subpath of a given key.
      */
     virtual std::string getSubpath(DataKey * key);
     virtual Type getType(DataKey * key) = 0;
     virtual std::string getDescription(DataKey * key) = 0;

     /**
      * Getters and setters. Although getters and setters are only called by the DataModelService
      * value changes are not automatically signaled from it.
      * Setters are never called for DataObjects not having the CAN_SET_VALUE flag in it's type.
      */
     /* only called for BOOLEAN or CUSTOM - type */
     virtual bool getBoolVal(DataKey * key) = 0;
     virtual void setBoolVal(DataKey * key, bool newValue) = 0;

     /* only called for INTEGER or CUSTOM - type */
     virtual int  getIntVal(DataKey * key) = 0;
     virtual void setIntVal(DataKey * key, int newValue) = 0;

     /* only called for FLOAT or CUSTOM - type */
     virtual float getFloatVal(DataKey * key) = 0;
     virtual void  setFloatVal(DataKey * key, float newValue) = 0;

     /* only called for STRING or CUSTOM - type */
     virtual std::string getStringVal(DataKey * key) = 0;
     virtual void        setStringVal(DataKey * key, std::string & newValue) = 0;

     /**
      * Adds a new child and returns the new DataKey of it. If it should be handled by some
      * other provider this has to be specified by a non-NULL value of DataModelProvider.
      */
     virtual DataKey * addChild(DataKey * parent, DataModelProvider * provider) = 0;
     virtual void removeChild(DataKey * child) = 0;
     virtual void getChilds(DataKey * parent, std::vector<DataKey *> & listOfChilds) = 0;
};

Predefined implementations of DataKey and DataModelProvider

To ease the pain of implementing the same thing over and over again (I mean reinventing and rebuilding the wheel) there are some DataModelProviders available to solve some common task.

The most common of all is the FolderProvider, which has the possibility to make an arbitrary structure containing no data, but DataObjects of other DataModelProviders as it's leaves. The same is used for the top of the Data Model tree. You can also make it modifiable by DataModelUsers or not if you want.

You can also use this one for lists, but there is also an extra provider that acts like a std::list: ListProvider. You only have to set the type of the DataModel for the child nodes.

Furthermore there is BoolProvider, IntProvider, StrProvider and FloatProvider behaving like bool, int, std::string and float. The advantage of using this ones is, that they are automatically firing change events and are very comfortable replacing the types they represent, because they have overloaded operators.

At least but not last there is a DataModelProvider for links: LinkProvider. This is automatically used whenever a link is needed. It just forwards most of the interface functions to the provider of the link destination.

Also if you're going to implement your own DataModelProvider and you want you're objects to be mapped to integers, you can use IntKey or StrKey - special inheritances of DataKey.

DataModelService interface

At last but not least the DataModelService interface should be presented. This is used by Data Model Users as well as Data Model providers.

class DataModelService
{
    /**
     * Functions for Data Model Users
     */
    DataObject * getRoot();
    DataObject * getByPath(std::string path);

    /**
     * Functions for Data Model Providers
     */
    void fireSignal(DataModelProvider * provider, std::string subpath, Type event);
    FolderProvider * getRootProvider();
};