15. ORM Part 3: Interface Class

After defining object models as explained in ORM Part 2: Defining Object and building the project, iFun Engine automatically generates matching ORM classes. Such a auto-generated ORM class is called interface class.

Say, the project name is {{ProjectName}} and the name of an object model in JSON is {{ObjectName}}, auto-generated files including an interface class is like follows:

  • src/{{ProjectName}}_object.h

  • src/object_model/{{ProjectName}}_object.cc

  • src/object_model/common.h

  • src/object_model/{{ObjectName}}.h

  • mono/{{ProjectName}}_object.cs

Note

Auto-generated files are put under the source directory, rather than the build directory. This is to help IDE like Visual Studio perform auto-completion properly.

Warning

Interface class must be used within only in the event threads. Other threads cannot use the class directly. Instead, you can inject an asynchronous event using Event::Invoke(). Please refer to event threads for more information on the function.

15.1. Methods of Interface Class

This section assumes an object model like this:

"ObjectName" : {
  "KeyAttribute1Name" : "KeyAttribute1Type Key",
  "KeyAttribute2Name" : "KeyAttribute2Type Key",
  "Attribute3Name" : "Attribute3Type",
  "Attribute4Name" : "Attribute4Type"
}

Note

KeyAttribute1Type, KeyAttribute2Type, Attribute3Type, and Attribute4Type refer to either primitive types or other object types.

Note

In the list of functions below, prepending ROLLBACK keyword indicates the function can trigger an iFun Engine ORM ROLLBACK. For details, please see Transaction rollbacks.

15.1.1. Creating an object on DB

To create an object, methods like Create(…) are generated. If an object model has any fields with the Key flag, the Create method takes all the key fields.

static Ptr<ObjectName> Create(const KeyAttribute1Type &key1_value,
                              const KeyAttribute2Type &key2_value) ROLLBACK;
public static ObjectName Create(KeyAttribute1Type key1_value,
                                KeyAttribute2Type key2_value) ROLLBACK;

Important

If some attribute is set as Key, and there’s another object with the same attribute value, creation fails and the method returns NULL.

In the example below, ch1 and ch2 will be created successfully .But creating ch3 will fail because its Name, the key attribute, conflicts with one of ch1.

1
2
3
4
5
6
Ptr<Character> ch1 = Character::Create("legend");
Ptr<Character> ch2 = Character::Create("killer");

// ch3 has a Name, which is a Key field, the same as ch1. So, NULL will be returned.
Ptr<Character> ch3 = Character::Create("legend");
BOOST_ASSERT(not ch3);
1
2
3
4
5
6
Character ch1 = Character.Create ("legend");
Character ch2 = Character.Create ("killer");

// ch3 has a Name, which is a Key field, the same as ch1. So, NULL will be returned.
Character ch3 = Character.Create ("legend");
Log.Assert (ch3 == null);

15.1.2. Fetching objects from DB

There are two kinds of methods are generated to fetch objects. First one looks like Fetch(…) and the second one looks like FetchBy…(…). The former fetch an object using UUID regardless of object model, the latter is to fetch an object using one of its key attributes. In addition, each version has a single object form and a multi-object, batch form.

15.1.2.1. Fetching a single object using UUID

Like below, Fetch(…) can be used to read a single object using its UUID. If there’s no object associated with the UUID, null will be returned.

static Ptr<ObjectName> Fetch(
    const Object::Id &id,
    LockType lock_type = kWriteLock) ROLLBACK;
public static ObjectName Fetch(
    System.Guid object_id,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

15.1.2.2. Fetching multiple objects using UUIDs

Alos, it’s possible to fetch multiple objects in a batch manner like below:

If there’s no object associated with a particular UUID, returned *result will not have the UUID.

static void Fetch(
    const std::vector<Object::Id> &ids,
    std::vector<std::pair<Object::Id, Ptr<ObjectName> > > *result,
    LockType lock_type = kWriteLock) ROLLBACK;

If there’s no object associated with a particular UUID, returned Dictionary will not have the UUID.

public static Dictionary<System.Guid, ObjectName> Fetch(
    SortedSet<System.Guid> object_ids,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

15.1.2.3. Fetching a single object using a key field

If the object model has key attribute(s) (see Attribute Flag for the key flag), methods that fetch an object using each of its key attributes. If there’s no object with the given key value, null will be returned.

In the example below, you can see auto-generated fetch method for KeyAttribute1Name as tagged with the key flag.

static Ptr<ObjectName> FetchByKeyAttribute1Name(
    const KeyAttribute1Type &value,
    LockType lock_type = kWriteLock) ROLLBACK;
public static ObjectName FetchByKeyAttribute1Name(
    KeyAttribute1Type value,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

15.1.2.4. Fetching multiple objects using a key field

It’s also possible to fetch multiple objects using an array of keys in a batch manner.

If there’s no object associated with a particular key, returned *result will not have the key.

static void FetchByKeyAttribute1Name(
    const std::vector<KeyAttribute1Type> &values,
    std::vector<std::pair<KeyAttribute1Type, Ptr<ObjectName> > > *result,
    LockType lock_type = kWriteLock) ROLLBACK;

If there’s no object associated with a particular key, returned Dictionary will not have the key.

public static Dictionary<KeyAttribute1Type, ObjectName> FetchByKeyAttribute1Name(
    SortedSet<KeyAttribute1Type> values,
    funapi.LockType lock_type = funapi.LockType.kWriteLock) ROLLBACK;

15.1.2.5. Object caching on fetch

iFun Engine ORM caches objects for performance improvement. Considering the objects cache, fetch will be performed in this steps.

  1. If the local objects cache has the object, immediately returns after fetching from the cache.

  2. If the object cache at a remote server has the object, leases the object from the server via RPC and returns the object.

  3. If the target object is cached at any server, the server reads the object from the database and caches it in its local objects cache.

  4. If the target object does not exist even in the database, null is returned.

Note

Please refer to DB caching to understand when the ORM removes objects from the cache.

15.1.2.6. LockType when fetching

There are three kinds of LockType.

  • kWriteLock: To update an object or to delete an object. No other threads or servers can access the same object while this lock is held by a thread.

  • kReadLock: To read an object. Other threads or servers can simultaneously read the same object, but cannot update is prohibited.

  • kReadCopyNoLock: To make a duplicate instead of lock the object. Since a duplicate is invoked, writing does not make a sense and hence only reading is allowed. Because the current thread uses a duplicate, the target object can be accessed by other threads or server.

    Using a duplicate is especially efficient if it’s OK that the target object could become stable for a short time like 1 second. For the example of displaying the last seen time of a list of friends, this lock mode can improve concurrency because it avoids locking.

Important

kWriteLock is the default value. But you better prefer either kReadLock or kReadCopyNoLock over it to improve locking performance.

Example) Fetching an object using a key attribute Name:

Ptr<Character> ch = Character::FetchByName("legend")
Character ch = Character.FetchByName("legend");

Example) Fetching a list of objects using a key attribute Name, but preferring kReadCopyNoLock for better concurrency:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
std::vector<string> names;
names.push_back("name1");
names.push_back("name2");
names.push_back("name3");

std::vector<std::pair<string, Ptr<Character> > > object_pairs;
Character::FetchByName(names, &object_pairs, kReadCopyNoLock);

for (size_t i = 0; i < object_pairs.size(); ++i) {
  const string &name = object_pairs[i].first;
  const Ptr<Character> &ch = object_pairs[i].second;
  if (ch) {
    ...
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SortedSet<string> names = new SortedSet<string>();
names.Add("name1");
names.Add("name2");
names.Add("name3");

Dictionary<string, Character> object_dict = null;
object_dict = Character.FetchByName(
    names, funapi.LockType.kReadCopyNoLock);

foreach(KeyValuePair<string, Character> object_pair in object_dict)
{
  if (object_pair.Value) {
    ...
  }
}

Tip

For performance improvement related to the lock types, please see Appropriate lock type usage.

15.1.3. Deleting an object from DB

You can delete an object from the database using a method below:

void Delete();
public void Delete();

If the target object to delete has fields of other object types, the owned objects will be deleted cascaded. But if the fields are tagged using Foreign as explained in Attribute Flag, no cascade deletion happens.

Please aware that due to C++/C# syntax, iFun Engine cannot automatically set the variable holding the target object to delete to null even if it deletes the object from the database. For related description, please refer to Checking if object is valid below.

15.1.4. Checking if object is valid

  1. Generated class is accessed always with a smart pointer like Ptr<ObjectName>. To check if Ptr<ObjectName> is NULL, you can compare it to either ObjectName::kNullPtr or Ptr<ObjectName>().

  2. Unless Ptr<ObjectName> is the same as ObjectName::kNullPtr, the pointer is considered valid. Please be aware, however, that the pointer still remains valid while the target object does not exist if it is deleted from the database. This means we need to verify if the target object indeed exists like this:

bool IsNull() const;
  1. Generated class is accessed always as a variable of the ObjectName type. You can check if a variable is null by comparing it to C#’s null.

  2. Unless a variable of ObjectName type is null, the variable is considered valid. Please aware, however, that the variable still remains valid while the target obejct does not exist if it is deleted from the database. This means we need verify if the target object indeed exists like this:

public bool IsNull();

Example

1
2
3
4
5
6
7
Ptr<Character> ch = Character::FetchByName("legend");
if (not ch) {
  return;
}

ch->Delete();
BOOST_ASSERT(ch->IsNull());
1
2
3
4
5
6
7
Character ch = Character.FetchByName("legend");
if (ch == null) {
  return;
}

ch.Delete ();
Log.Assert (ch.IsNull());

15.1.5. Getting object ID

Every object is automatically assigned a unique ID in the form of UUID. To access the ID of an object, you can do like this:

const Object::Id &Id() const;
public System.Guid Id;

Since every object has a unique ID, it’s possible to access an object in the database using the auto-generated Fetch(...) method with the ID.

15.1.6. Read/Write object’s fields

For each field, both getter and setter are automatically generated. Getter is prepended a prefix Get and returns a value in the type matching the field type. Similarly, setter is prepended a prefix Set and takes an argument of the type matching the field type.

Here’s an example for the AttributeName3 case.

Attribute3Type GetAttribute3Name() const;
void SetAttribute3Name(const Attribute3Type &value);
public GetAttribute3Type GetAttribute3Name();
public void SetGetAttribute3Name(Attribute3Type value);

Example) This example increases the level and resets the experience point if the experience point reaches 100.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Ptr<Character> ch = Character::FetchByName("legend");
if (not ch) {
  return;
}

string char_name = ch->GetName();

if (ch->GetExp() > 100) {
  ch->SetLevel(ch->GetLevel() + 1);
  ch->SetExp(0);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Character ch = Character.FetchByName ("legend");
if (ch == null) {
  return;
}

string char_name = ch.GetName ();

if (ch.GetExp () > 100) {
  ch.SetLevel (ch.GetLevel() + 1);
  ch.SetExp (0);
}

Note

Once you update an object’s attribute, the change is reflected to both ORM cache and DB. If the object is one that has been leased from another server, the change will go to the leasing server’s ORM cache.

15.1.6.1. Array/Map without the Foreign flag

For array or map without the Foreign flag, a getter is generated like this:

  • Get{{AttributeName}}(): It performs cascaded fetches for all the elements, and then returns ArrayRef<Ptr<{ObjectType}> > or MapRef<{KeyType}, Ptr<{ObjectType}> >.

15.1.6.2. Array/Map with the Foreign flag

For array or map with the Foreign flag, two kinds of getter functions are generated like this:

  • Get{{AttributeName}(): It does not perform cascaded fetches. Instead, it contains object Ids if elements are objects. And then it returns ArrayRef<Object::Id> or MapRef<{KeyType}, Object::Id>.

  • Fetch{AttributeName}(): It performs cascaded fetches for all the elements, and then returns ArrayRef<Ptr<{{ObjectType}}> > or MapRef<{{KeyType}}, Ptr<{{ObjectType}}> >.

15.1.7. Refreshing a pointer to object

In a distributed environment with multiple servers, iFun Engine leases objects that are cached at a remote server, if any, instead of using the database as a synchronization point. This effectively reduces an overhead to the database.

Because of the behavior, an object that is created by Create(...) or an object that is fetched by either Fetch(...) or FetchBy...(...) is guaranteed to be valid only within an event handler that creates/fetches the object and uses the object at the same time. This means that a pointer to an object that is created/fetched in an event handler might be invalidated once the code flow leaves the event handler, and that the object might be leased to another thread or another server. On iFun Engine, therefore, it’s recommended to call Create(…) / Fetch(…) FetchBy…(…) an object using its ID or key just before using the object in an event handler.

If you have to store a pointer to an object and reuses the pointer (e.g., storing a globally shared object in a global variable), you need to refresh the pointer in this way:

bool IsFresh() const;

bool Refresh() ROLLBACK;
public bool IsFresh();

public bool Refresh(funapi.LockType lock_type) ROLLBACK;

IsFresh checks if an object pointed to by a pointer is still valid. If IsFresh() returns false, you need to refresh the pointer using Refresh().

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
std::map<string /*account id*/, Ptr<Character> > g_characters;

void OnLevelRequested(const Ptr<Session> &session, const Json &message) {
  string account_id = message["account_id"].GetString();

  Ptr<Character> ch = g_characters.find(account_id);

  // Checks if the object pointed to by this pointer is valid. Refreshes if not.
  if (not ch->IsFresh()) {
    ch->Refresh();
  }

  Json response;
  response["level"] = ch->GetLevel();

  session->SendMessage("level", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Dictionary<string, Character> the_characters;

public void OnLevelRequested(Session session, JObject message) {
  string account_id = (string) message ["account_id"];

  Character ch = null;
  if (the_characters.TryGetValue (account_id, out ch))
  {
    return;
  }

  // Checks if the object stored in this variable is valid. Refreshes if not.
  if (!ch.IsFresh ())
  {
    ch.Refresh (funapi.LockType.kWriteLock);
  }

  JObject response = new JObject ();
  response ["level"] = ch.GetLevel ();

  session.SendMessage ("level", response);
}

15.1.8. Initializing an object from JSON data

We may be able to initialize an object using the setter functions of the object. This surely works, but not very efficient if the object has many attributes (therefore needs to call many setters). To handle this case, iFun Engine ORM generates a method that can initialize an object from JSON data.

Tip

This is very helpful especially when creating a new player data from default values.

Also, this is helpful for a game designer to adjust initial player data without a help from programmers, because the data is read from a JSON file. As a related material, please see Content support part 4: Game design data to handle game data in JSON.

struct OpaqueData;
static Ptr<OpaqueData> CreateOpaqueDataFromJson(const Json &json);

bool PopulateFrom(const Ptr &opaque_data);

Note

Not supported, yet.

To initialize an object from JSON data, you need to perform following two steps.

  1. Creating a opaque data for the object type using its CreateOpaqueDataFromJson(...) method.

  2. Initializing an object from the generated OpaqueData using the object’s PopulateFrom(opaque_data) method.

For JSON data, you simply mimic the structure of its ORM schema in JSON. That is, it would look like {"field name": "value"}. If a filed is not a primitive type, enter value in this way:

  • For an Array, use the JSON Array format.

  • For a Map, use the JSON Object format.

  • For another object, use the JSON Object format according to its ORM definition.

    Important

    If a field is of other object type, the object must not have any key attribute.

    It’s because each object must be assigned a unique key, while this method simply duplicates an object with the same key.

Example: Initializing a player data according to player class

character_init_data.json

{
  "Warrior": {
    "Level": 1,
    "Exp": 0,
    "Hp": 1000,
    "Mp": 100
  },
  "Wizard": {
    "Level": 1,
    "Exp": 0,
    "Hp": 100,
    "Mp": 1000
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  // Let's assume the message variable contains a field named character_name.
  string character_name = message["character_name"].GetString();

  // 1. Create a Character object.
  Ptr<Character> ch = Character::Create(character_name);

  if (ch) {
    Ptr<const Json> json_data = ResourceManager::GetJsonData("character_init_data.json");
    BOOST_ASSERT(json_data);

    // We will initialize as a "Warrior".
    // Substitute "Warrior" with "Wizard" if you want to create a wizard.
    // 1. Create an OpaqueData from the JSON data.
    Ptr<Character::OpaqueData> opaque_data =
      Character::CreateOpaqueDataFromJson((*json_data)["Warrior"]);
    BOOST_ASSERT(opaque_data);

    // 2. Passes the OpaqueData to the newly created Character object and initializes the object.
    bool success = ch->PopulateFrom(opaque_data);
    BOOST_ASSERT(success);
  }
}

Note

Not supported, yet.

15.2. Array & Map

We studied that iFun Engine ORM generates a getter and a setter for each field of an object type in Read/Write object’s fields. For a primitive type, its getter returns a value of the type and its setter takes a parameter of the type. For an array type or a map type, however, its getter and setter handle an array of the type or a map of the type, respectively.

15.2.1. Array: ArrayRef<Type>

We know that we will get an array if we append [ ] to a type name like Type[ ]. The array type will look like ArrayRef<Type> in C++.

For example, String[] will be ArrayRef<string>, and Character[] will be ArrayRef< Ptr<Character> > in C++.

ArrayRef<Type> has methods as follows:

15.2.1.1. Getting a size

To get the number of elements in an array, use this method.

size_t Size() const
UInt64 Length

15.2.1.2. Checks if element at a specific index

To check if there’s an element with a specific index value, use this method. Index is valid as [0, SIZE - 1]. If a given index is invalid, iFun Engine will terminate with a FATAL log message.

bool Has(size_t index) const
bool Has(ulong index)

15.2.1.3. Getting an element at a specific index

To get an element at a specific location, use this method. Index is valid as [0, SIZE - 1]. If a given index is invalid, iFun Engine will terminate with a FATAL log message.

T GetAt(size_t index) const
T GetAt(ulong index)

15.2.1.4. Writing an element at a specific index

To set an element at a specific location, use this method. Index is valid as [0, SIZE - 1]. If a given index is invalid, iFun Engine will terminate with a FATAL log message.

void SetAt(size_t index, const T &value)
void SetAt(ulong index, T value)

15.2.1.5. Adding an element at a specific index

To add an element at a specific location, use this method. After this operation, the length of the array will increase. Index is valid as [0, SIZE - 1]. If a given index is invalid, iFun Engine will terminate with a FATAL log message.

Important

If you add an element in the middle of an array using InsertAt(...), it will move elements, which will cause an overhead.

void InsertAt(size_t index, const T &value)
void InsertAt(ulong index, T value)

15.2.1.6. Deleting an object at a specific location

To remove an element from an array and reduces its size, use this method. Index is valid as [0, SIZE - 1]. If a given index is invalid, iFun Engine will terminate with a FATAL log message.

If an element is of an object type and delete_object is passed as true, it will call the object type’s Delete() method and perform cascaded deletion.

Important

If you remove an element from a middle of an array using EraseAt(...), it will moves elements and cause an overhead.

void EraseAt(size_t index, bool delete_object = true)
void EraseAt(ulong index, bool delete_object = true)

15.2.1.7. Clearing an array

To make an array empty and set its size to zero, use this method.

If an element is of an object type and delete_object is passed as true, it will call the object type’s Delete() method and perform cascaded deletion.

void Clear(bool delete_object = true);
void Clear(bool delete_object = true)

15.2.1.8. (Shortcut) Accessing the first element

This method returns the first element in an array. This is the same as GetAt(0). If an array is empty, iFun Engine will terminate with a FATAL log message.

T Front() const;
T Front()

15.2.1.9. (Shortcut) Accessing the last element

This method returns the last element in an array, this is the same as Get(SIZE - 1). If an array is empty, iFun Engine will terminate with a FATAL log message.

T Back() const;
T Back()

15.2.1.10. (Shortcut) Prepending an element to an array

This method prepends an element and increases the length of the array. It is the same as InsertAT(0, ...).

void PushFront(const T &value);
void PushFront(T value)

15.2.1.11. (Shortcut) Appending an element to an array

This method appends an element and increases the length of the array. It is the same as InsertAt(SIZE, ...).

void PushBack(const T &value);
void PushBack(T value)

15.2.1.12. (Shortcut) Finding the first empty slot

This method finds the first empty slot. If there’s no slot, it returns -1.

Important

If an element value matches one of the following, it will treated as an empty slot.

자료형

기본값

Bool

false

Integer

0

Double

0.0

String

“”

DateTime

1970-01-01 00:00:00 UTC

User-Defined Object

null

int64_t FindFirstEmptySlot() const;
long FindFirstEmptySlot()

15.2.2. Map: MapRef<KeyType, ValueType>

We know that we will get a map if define like <KeyType, ValueType>. For KeyType, only primitive types like Bool, Integer, Double, and String(n) are allowed, while both primitive types and other object types can be used as ValueType. From the map definition, MapRef<KeyType, ValueType> is created in C++.

For example, <Integer, String> will be in MapRef<int64_t, string> in C++, and <String, Character> will be MapRef<string, Ptr<Character> > in C++.

MapRef<KeyType, ValueType> has methods as follows:

15.2.2.1. Checking if value for a specific key

This method checks if there’s a value for a given key.

bool Has(const KeyType &key) const
bool Has(KeyType key)

15.2.2.2. Reading a value for a specific key

This methods reads a value for a given key. If the key does not exist in the map, iFun Engine terminates with a FATAL log message.

ValueType GetAt(const KeyType &key) const
ValueType GetAt(KeyType key)

15.2.2.3. Setting a value for a specific key

This method sets a value for a specific key. If the key exists, value will be replaced with the new value. Otherwise, the new key is added with the value.

void SetAt(const KeyType &key, const ValueType &value)
void SetAt(KeyType key, ValueType value)

15.2.2.4. Deleting an object for a specific key

This method deletes a value for a given key. If value is of an object type and delete_object is passed as true, it will call the object type’s Delete() method and perform cascaded deletion.

bool EraseAt(const KeyType &key, bool delete_object = true)
bool EraseAt(KeyType key, bool delete_object = true)

15.2.2.5. Clearing the map

This method clears the map and sets the size to zero. If value is of an object type and delete_object is passed as true, it will call the object type’s Delete() method and perform cascaded deletion.

void Clear(bool delete_object = true)
void Clear(bool delete_object = true)

15.2.2.6. Retrieving all the keys

This method returns a list of keys in the map.

Tip

This operation may be expensive in terms of performance. So, it’s recommended to store the results somewhere, instead of invoking the operation wherever needed.

std::vector<KeyType> Keys() const
SortedSet<TKey> Keys

15.2.2.7. Getting a size

To get the number of elements in a mapref, use the below method.

size_t Size() const
UInt64 Length

Note

The Size() function is available in version 1.0.0-2885 Experimental and higher.

15.2.2.8. (Shortcut) Finding the smallest integer key not in the map whose key is of integer

Important

This applies only to a map whose KeyType is Integer.

This method searches the smallest integer key not in the map by trying from 0.

int64_t FindFirstEmptySlot() const
long FindFirstEmptySlot()

15.3. Interface Class Usage Examples

15.3.1. Initializing an player inventory

In the example below, we create a character and an inventory whose capacity is 10.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  // Creates 10 empty slots in the Inventory.
  for (size_t i = 0; i < 10; ++i) {
    // Item::kNullPtr is the same as Ptr<Item>.
    inventory.PushBack(Item::kNullPtr);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();

  // Creates 10 empty slots in the Inventory.
  for (int i = 0; i < 10; ++i)
  {
    inventory.PushBack(null);
  }
}

15.3.2. Adding an item to an inventory

In the example below, we fetch a Character object and add a new item at the 3rd slot of the Character’s inventory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void AddItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  Ptr<Item> item = Item::Create();
  inventory.SetAt(3, item);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void AddItem()
{
  Character ch = Character.FetchByName("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory();

  Item item = Item.Create();
  inventory.SetAt(3, item);
}

15.3.3. Removing an item from the inventory

In the example below, we remove and destroy an item at the 7th slot of the Character’s inventory. If you want to move the item to other’s inventory, you can delete the destroy code.

Important

Please note that we use SetAt(index, null) instead of EraseAt(index) to remove an item from the inventory. Since the latter reduces the size of the inventory array, it’s effectively same as shirking the inventory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void DeleteItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  Ptr<Item> item = inventory.GetAt(7);
  if (item) {
    inventory.SetAt(7, Item::kNullPtr);
    item->Delete();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void DeleteItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();

  Item item = inventory.GetAt (7);
  if (item != null)
  {
    inventory.SetAt (7, null);
    item.Delete ();
  }
}

15.3.4. Adding an item to an empty slot

In this example, we add a new item to an empty slot of the inventory. If there’s no empty slot, we increases the size of the inventory and add an item.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void AddItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  Ptr<Item> item = Item::Create();

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();

  int64_t empty_slot_index = inventory.FindFirstEmptySlot();
  if (empty_slot_index == -1) {
    inventory.PushBack(item);
  } else {
    inventory.SetAt(empty_slot_index, item);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void AddItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  Item item = Item.Create();

  ArrayRef<Item> inventory = ch.GetInventory();

  long empty_slot_index = inventory.FindFirstEmptySlot();
  if (empty_slot_index == -1) {
    inventory.PushBack(item);
  }
  else
  {
    inventory.SetAt(empty_slot_index, item);
  }
}

15.3.5. Managing equipment slots

In this example, we create equipment slots for head, chest, right hand, and left hand.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  MapRef<string, Ptr<Item> > equips = ch->GetEquippedItem();

  equips.SetAt("head", Item::kNullPtr);
  equips.SetAt("chest", Item::kNullPtr);
  equips.SetAt("righthand", Item::kNullPtr);
  equips.SetAt("lefthand", Item::kNullPtr);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  MapRef<string, Item> equips = ch.GetEquippedItem();

  equips.SetAt ("head", null);
  equips.SetAt ("chest", null);
  equips.SetAt ("righthand", null);
  equips.SetAt ("lefthand", null);
}

15.3.6. Equipping an item from the inventory

In the example below, we equip an item that was previously at the 5th slot of the inventory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void EquipWithItem() {
  Ptr<Character> ch = Character::FetchByName("legend");
  if (not ch) {
    return;
  }

  ArrayRef<Ptr<Item> > inventory = ch->GetInventory();
  MapRef<string, Ptr<Item> > equips = ch->GetEquippedItem();

  if (inventory.Has(5)) {
    Ptr<Item> item = inventory.GetAt(5);
    inventory.SetAt(5, Item::kNullPtr);
    equips.SetAt("righthand", item);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public void EquipWithItem()
{
  Character ch = Character.FetchByName ("legend");
  if (ch == null)
  {
    return;
  }

  ArrayRef<Item> inventory = ch.GetInventory ();
  MapRef<string, Item> equips = ch.GetEquippedItem ();

  if (inventory.Has (5))
  {
    Item item = inventory.GetAt (5);
    inventory.SetAt (5, null);
    equips.SetAt ("righthand", item);
  }
}

15.3.7. Registering an item with an empty quick slot

In this example, we register an item in the inventory to an empty quick slot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void CreateCharacter() {
  Ptr<Character> ch = Character::Create("legend");
  if (not ch) {
    return;
  }

  Ptr<Item> item = Item::Create();

  MapRef<int64_t, Ptr<Item> > quick_slot = ch->GetQuickSlot();

  int64_t empty_slot_index = quick_slot.FindFirstEmptySlot();
  quick_slot.SetAt(empty_slot_index, item);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void CreateCharacter()
{
  Character ch = Character.Create ("legend");
  if (ch == null)
  {
    return;
  }

  Item item = Item.Create();

  MapRef<ulong, Item > quick_slot = ch.GetQuickSlot ();

  long empty_slot_index = quick_slot.FindFirstEmptySlot();
  quick_slot.SetAt(empty_slot_index, item);
}

15.3.8. Creating a character

In the example, we receive a character creation packet from the client and create a Character instance from the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  string character_name = message["character_name"].GetString();
  LOG(INFO) << "chracter name : " << character_name;

  Ptr<Character> ch = Character::Create(character_name);

  Json response;

  if (ch) {
    // Character object has been created.
    // Sets the default stats and gives a novice sword
    response["result"] = true;
    ch->SetLevel(1);
    ch->SetHp(100);
    ch->SetMp(100);
    ch->SetExp(0);

    Ptr<Item> novice_sword = Item::Create();

    Ptr<Item> hp_potion = Item::Create();

    // Gives a sword to the first slot of the Inventory.
    ch->GetInventory().InsertAt(0, novice_sword);

    // Equips the sword at the right hand slot.
    ch->GetEquippedItem().SetAt("RightHand", novice_sword);

    // Caution) novice_sword in the Inventory has also been assigned to EquippedItem.
    // (The inventory slot and the equipment slot point to the same object, and hence it does not duplicate.)

    // Adds an HP potion to the first quick slot.
    ch->GetQuickSlot().SetAt(0, hp_potion);
  } else {
    // The character name has been already taken.
    response["result"] = false;
    response["reason"] = character_name + " already exists";
  }

  session->SendMessage("create_character", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void OnCreateCharacter(Session session, JObject message) {
  string character_name = (string) message["character_name"];
  Log.Info("chracter name = {0}", character_name);

  Character ch = Chracter.Create(character_name);
  JObject response = new JObject ();

  if (ch != null) {
    // Character object has been created.
    // Sets the default stats and gives a novice sword
    response ["result"] = true;
    ch.SetLevel (1);
    ch.SetHp (100);
    ch.SetMp (100);
    ch.SetExp (0);

    Item novice_sword = Item.Create ();

    Item hp_potion = Item.Create ();

    // Gives a sword to the first slot of the Inventory.
    ch.GetInventory().InsertAt (0, novice_sword);

    // Equips the sword at the right hand slot.
    ch.GetEquippedItem().SetAt ("RightHand", novice_sword);

    // Caution) novice_sword in the Inventory has also been assigned to EquippedItem.
    // (The inventory slot and the equipment slot point to the same object, and hence it does not duplicate.)

    // Adds an HP potion to the first quick slot.
    ch.GetQuickSlot().SetAt (0, hp_potion);
  }
  else
  {
    // The character name has been already taken.
    response ["result"] = false;
    response ["reason"] = character_name + " already exists";
  }

  session.SendMessage ("create_character", response);
}

15.3.9. Increasing a character’s experience and level

In this example, we increase the experience point by 100. If the point exceeds 3000, we increase the level point by 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void OnKill(const Ptr<Session> &session, const Json &msg) {
  string id = msg["UserId"].GetString();
  string ch_name = msg["CharacterName"].GetString();

  Ptr<User> user = User::FetchById(id);
  if (not user) {
    return;
  }

  Ptr<Character> ch = user->GetMyCharacter();
  if (not ch) {
    return;
  }

  ch->SetExp(ch->GetExp() + 100);
  if (ch->GetExp() >= 3000) {
    ch->SetLevel(ch->GetLevel() + 1);
    ch->SetExp(ch->GetExp() - 3000);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void OnKill(Session session, JObject message)
{
  string id = (string) message ["UserId"];
  string ch_name = (string) message ["CharacterName"];

  User user = User.FetchById (id);
  if (user == null)
  {
    return;
  }

  Character ch = user.GetMyCharacter ();
  if (ch == null)
  {
    return;
  }

  ch.SetExp (ch.GetExp () + 100);
  if (ch.GetExp () >= 3000)
  {
    ch.SetLevel (ch.GetLevel () + 1);
    ch.SetExp (ch.GetExp () - 3000);
  }
}