Skip to content

Creating items for Store

alongubkin edited this page Feb 18, 2013 · 37 revisions

This tutorial will focus on creating items for the Store plugin. It assumes that you have basic SourceMod development knowledge and a working server with Store installed.

In this tutorial we will build one-time items called Sounds. When used in your inventory, they will play a sound to everyone in the server.

Introduction

In Store, items and categories are defined in the database. That means that you don't need to write any code to add basic items to your server. However, if you go ahead and actually try that, you'll notice that when you click on the item you've just added in the inventory, nothing happens.

The reason for that is while basic properties of an item (such as name, price or description) are defined in the database, the behavior of the item is not. And by 'behavior of an item', I mean what happens when you click on it in your inventory. The behavior of an item is defined by a plugin.

Creating an item consists of 2 steps:

  • Adding the item to the database, using the web panel.
  • Creating a plugin that defines the behavior of the item.

Adding items to the database

For this step, you will need the Store Web Panel installed.

Let's get started by adding our category. Open the panel and navigate to Add New Category under the Categories menu. In Display Name type Sounds. You can also fill the description field if you want. Leave the rest empty for now, and click Add.

Adding category.

Next, we will need to add the items themselves. The first sound that we'll add is the Bass sound, which should be available in all Source games. Navigate to Add New Item under the Items menu. Here is an explanation of each field in this page:

  • Name - This is the unique identifier of the item. Should be lowered-case, no spaces, and maximum 32 characters. Type sound_bass.
  • Display Name - This is the name of the item as players will see it in-game. Maximum 64 characters and can be anything (including UTF-8). Type Bass for now.
  • Description - A short description of the item that will appear in the shop. Type anything here or leave it empty.
  • Type - This is the relation between the item we're adding and the plugin that we're going to write later. It defines our item's behavior. Should be lowered-case, no spaces, and maximum 32 characters. Type sound in this field. When players will click on our item in their inventory, the store will search for a plugin that registers the sound item type, and call it.
  • Loadout Slot - Wearable items (such as hats) use this field for specifying the loadout slot that the item should go to when equipped. In each loadout slot, a player can wear one item. Since sounds aren't wearable, leave this field empty.
  • Attributes - Custom properties that are unique to your item type (for example: sound path). Usually, you'd want to use JSON or XML in this field. Right now, for the sake of simplicity - leave it empty. We will get to attributes later.

Adding item.

When you're done, click Save Changes.

Enter your server and type store_reloaditems in the console. Then, open !shop, and you can see now our new category:

new category

When you click on it, you can also see our new item:

new item

Buy the item, and open your !inventory. When you try to use the item, you should see the following message:

The item type 'sound' wasn't registered by any plugin.

This is happening because the store is trying to find a plugin that registers the sound item type, and there are none. So our next step, is to write a plugin that registers the sound item type and defines the item behavior when it is used.

Writing the plugin

For this step, you will need to know how to write and compile basic SourceMod plugins. If you don't, read the Scripting Tutorials in SourceMod wiki. They are great!

Let's start from a basic SourceMod plugin template:

#include <sourcemod>

public OnPluginStart()
{

}

The first thing that you'll want to do is to include the store APIs:

#include <sourcemod>
#include <store>

public OnPluginStart()
{

}

To register an item type, use the Store_RegisterItemType API. Here is its defintion:

/**
 * Registers an item type. 
 *
 * A type of an item defines its behaviour. Once you register a type, 
 * the store will provide two callbacks for you:
 * 	- Use callback: called when a player selects your item in his inventory.
 *	- Attributes callback: called when the store loads the attributes of your item (optional).
 *
 * It is recommended that each plugin registers *one* item type. 
 *
 * @param type			Item type unique identifer - maximum 32 characters, no whitespaces, lower case only.
 * @param useCallback	Called when a player selects your item in his inventory.
 * @param attrsCallback	Called when the store loads the attributes of your item.
 *
 * @noreturn
 */
native Store_RegisterItemType(const String:type[], Store_ItemUseCallback:useCallback, Store_ItemGetAttributesCallback:attrsCallback = Store_ItemGetAttributesCallback:0);

Our plugin now looks like:

#include <sourcemod>
#include <store>

public OnPluginStart()
{
    Store_RegisterItemType("sound", OnSoundUse);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse);
    }	
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    return Store_DeleteItem;
}

Take a look at the return Store_DeleteItem in OnSoundUse. If the callback returns Store_DeleteItem, then the item will be deleted from the player's inventory. There are more options available, but we won't discuss them here.

To play our sound, lets use EmitSoundToAll. We are also required to precache the sound:

#include <sourcemod>
#include <sdktools>
#include <store>

public OnPluginStart()
{
    Store_RegisterItemType("sound", OnSoundUse);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse);
    }	
}

public OnMapStart()
{
    PrecacheSound("common/bass.wav");
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    EmitSoundToAll("common/bass.wav");
    return Store_DeleteItem;
}

Now let's print a chat message to all players when a sound is played. We also want to include the display name of the sound as it is in the database in our message. The OnSoundUse provides an itemId, which we can use for retrieving that data, with the Store_GetItemDisplayName API.

#include <sourcemod>
#include <sdktools>
#include <store>

public OnPluginStart()
{
    Store_RegisterItemType("sound", OnSoundUse);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse);
    }	
}

public OnMapStart()
{
    PrecacheSound("common/bass.wav");
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    decl String:playerName[32];
    GetClientName(client, String:playerName, sizeof(playerName));

    decl String:soundDisplayName[STORE_MAX_DISPLAY_NAME_LENGTH];
    Store_GetItemDisplayName(itemId, soundDisplayName, sizeof(soundDisplayName));

    PrintToChatAll("%s has played %s!", playerName, soundDisplayName);

    EmitSoundToAll("common/bass.wav");
    return Store_DeleteItem;
}

You can try to add more sounds to the database. But if an item has the type sound, it will always play common/bass.wav.

Our purpose is now to add multiple sound support for our plugin. For each sound, we will specify its path in the attributes field in the database. Open the web panel again, and navigate to Manage Items under the Items menu. Find our Bass item, and in the attributes field, type common/bass.wav. Go ahead and add more sounds, using different paths.

Note: Usually, you'd want to use JSON or XML instead of plain string in attributes. We are just doing it now for the sake of simplicity.

The Store_RegisterItemType API also provides a callback for when item attributes are loaded. Let's add it:

#include <sourcemod>
#include <sdktools>
#include <store>

public OnPluginStart()
{
    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
    }	
}

// This will be called when the attributes are loaded.
public OnSoundAttributesLoad(const String:itemName[], const String:attrs[])
{
    PrecacheSound(attrs);
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    decl String:playerName[32];
    GetClientName(client, String:playerName, sizeof(playerName));

    decl String:soundDisplayName[STORE_MAX_DISPLAY_NAME_LENGTH];
    Store_GetItemDisplayName(itemId, soundDisplayName, sizeof(soundDisplayName));

    PrintToChatAll("%s has played %s!", playerName, soundDisplayName);

    EmitSoundToAll("common/bass.wav");
    return Store_DeleteItem;
}

We will use a Trie to store the sound paths:

#include <sourcemod>
#include <sdktools>
#include <store>

new Handle:g_soundPaths = INVALID_HANDLE;

public OnPluginStart()
{
    g_soundPaths = CreateTrie();
    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
    }	
}

// This will be called when the attributes are loaded.
public OnSoundAttributesLoad(const String:itemName[], const String:attrs[])
{
    PrecacheSound(attrs);
    SetTrieString(g_soundPaths, itemName, attrs);
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    decl String:playerName[32];
    GetClientName(client, String:playerName, sizeof(playerName));

    decl String:soundDisplayName[STORE_MAX_DISPLAY_NAME_LENGTH];
    Store_GetItemDisplayName(itemId, soundDisplayName, sizeof(soundDisplayName));

    PrintToChatAll("%s has played %s!", playerName, soundDisplayName);

    EmitSoundToAll("common/bass.wav");
    return Store_DeleteItem;
}

Now, we will need to retrieve the sound path in the OnSoundUse callback. To do that, we will need the name of the item that is being used. Note: We already have the display name of the item, now we need to get its name (the 32-chars unique identifier of the item).

To retrieve the item name, we will use Store_GetItemName.

#include <sourcemod>
#include <sdktools>
#include <store>

new Handle:g_soundPaths = INVALID_HANDLE;

public OnPluginStart()
{
    g_soundPaths = CreateTrie();
    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
    }	
}

// This will be called when the attributes are loaded.
public OnSoundAttributesLoad(const String:itemName[], const String:attrs[])
{
    PrecacheSound(attrs);
    SetTrieString(g_soundPaths, itemName, attrs);
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    decl String:playerName[32];
    GetClientName(client, String:playerName, sizeof(playerName));

    decl String:soundDisplayName[STORE_MAX_DISPLAY_NAME_LENGTH];
    Store_GetItemDisplayName(itemId, soundDisplayName, sizeof(soundDisplayName));

    PrintToChatAll("%s has played %s!", playerName, soundDisplayName);

    decl String:soundName[STORE_MAX_NAME_LENGTH];
    Store_GetItemName(itemId, soundName, sizeof(soundName));

    EmitSoundToAll(soundName);
    return Store_DeleteItem;
}

The last thing that you'd probably want to do is to add the sound path to the download list, using AddFileToDownloadsTable. Add that to OnSoundAttributesLoad:

#include <sourcemod>
#include <sdktools>
#include <store>

new Handle:g_soundPaths = INVALID_HANDLE;

public OnPluginStart()
{
    g_soundPaths = CreateTrie();
    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "store-inventory"))
    {
	    Store_RegisterItemType("sound", OnSoundUse, OnSoundAttributesLoad);
    }	
}

// This will be called when the attributes are loaded.
public OnSoundAttributesLoad(const String:itemName[], const String:soundPath[])
{
    PrecacheSound(soundPath);
    SetTrieString(g_soundPaths, itemName, soundPath);
    AddFileToDownloadList(soundPath);
}

// This will be called when players use our item in their inventory.
public Store_ItemUseAction:OnSoundUse(client, itemId, bool:equipped)
{
    decl String:playerName[32];
    GetClientName(client, String:playerName, sizeof(playerName));

    decl String:soundDisplayName[STORE_MAX_DISPLAY_NAME_LENGTH];
    Store_GetItemDisplayName(itemId, soundDisplayName, sizeof(soundDisplayName));

    PrintToChatAll("%s has played %s!", playerName, soundDisplayName);

    decl String:soundName[STORE_MAX_NAME_LENGTH];
    Store_GetItemName(itemId, soundName, sizeof(soundName));

    EmitSoundToAll(soundName);
    return Store_DeleteItem;
}

Conclusion

This tutorial should have given you a general idea of how to create items for Store.

Hope you enjoyed the tutorial. If you have any questions, you can ask me at the AlliedModders plugin thread.