Skip to content

Using Data Buckets

What are Data Buckets?

  • Data buckets are a replacement to the well-known qglobals, but they are far more performant, reliable and simpler to use
  • You can use data buckets to store values unique to anything you would like, for example
  • Bot based flags
  • Character based flags
  • NPC based flags
  • Zone based flags
  • etc.


Data buckets exist in these 6 main functions in both Perl and Lua.


quest::set_data(bucket_key, bucket_value, expires_in);



eq.set_data(bucket_key, bucket_value, expires_in);

mob:SetBucket(bucket_key, bucket_value, expires_in);

There are also 5 secondary functions in both Perl and Lua.








  • Data buckets are stored in the [[data_buckets]] table and has a very simple structure
Column Description
id Unique Data Bucket Identifier
key Unique Data Bucket Key
value Data Bucket Value
expires Data Bucket Expiration (UNIX Timestamp)
  • Expired data bucket rows will not be queryable via the quest API, they may exist in a table until 5-10 minutes have past and the server will garbage collect and wipe the table clean of expired buckets

Usage Examples


  • In this example you can see that we are doing the following:
  • Keying by character ID to set the flag to make this unique per player
  • Setting it with the value of 70
  • Leaving out the duration parameter means the bucket never expires
if ($text =~/character-flag-test/i) {
    my $bucket_key = $client->CharacterID() . "-epic-points";
    quest::set_data($bucket_key, 70);

    my $bucket_value = quest::get_data($bucket_key);
    quest::say("You have traveled far! You have a mighty ($bucket_value) epic points!");


  • In this example you can see that we are doing the following:
  • Setting a global flag
  • Immediately accessing it
  • Deleting it
  • Trying to unsuccessfully access it again because the bucket data had already been deleted
if (e.message:findi("test")) then
    e.self:Say("This is a test!")

    eq.set_data("lua_test_key", "lua_value");

    e.self:Say("We just set some bucket data with '".. eq.get_data("lua_test_key") .. "'");


    e.self:Say("I'm going to try and access the value again... '".. eq.get_data("lua_test_key") .. "'");

Ways to Key Buckets

Keying is simply a way to uniquely identify a flag, if you want to make some data unique to a player, then you would need something to key uniquely to that player, such as their character_id. If you wanted to set a flag uniquely for a NPC for example, you could use the npc_type_id, for a zone you could use the zone_id. All of these circumstances are completely up to you and you have the entire Quest API to grab something that can make something unique!

Some of the examples below should give you some ideas!

By Character

Manual Keying

my $bucket_key = $client->CharacterID() . "-some-flag";
my $bucket_value = 70;
quest::set_data($bucket_key, $bucket_value);

Automatic Keying

my $bucket_key = "some-flag";
my $bucket_value = 70;
$client->SetBucket($bucket_key, $bucket_value);

By Door (And Zone)

    if ($doorid == 4) {
        my $bucket_key  = "$zoneid-$doorid-last-person-to-click-door";
        my $bucket_value = $client->GetCleanName();

        if (quest::get_data($bucket_key)) {
            $client->Message(15, "You know... the last person to click this door was '" . quest::get_data($bucket_key) . "'");

        quest::set_data($bucket_key, $bucket_value);


Database Result


Manual Keying

    my $bucket_key = $npc->GetNPCTypeID() . "-death-count";
    quest::set_data($bucket_key, quest::get_data($bucket_key) + 1);

    my $death_count = quest::get_data($bucket_key);
    quest::shout("Man! I've died ($death_count) times in my lifetime!");

Automatic Keying

    my $bucket_key = "death-count";
    $npc->SetBucket($bucket_key, $npc->GetBucket($bucket_key) + 1);

    my $death_count = $npc->GetBucket($bucket_key);
    quest::shout("Man! I've died ($death_count) times in my lifetime!");



Automatically Keying Buckets

  • Automatic keying is a Mob only thing, you will still have to manually key anything else.
  • These methods automatically grab a key based on Mob type, these keys are formatted as follows:
  • Bots: bot-BotID
  • Clients: character-CharacterID
  • NPCs: npc-NPCID

  • DeleteBucket(bucket_key)

  • GetBucket(bucket_key)
  • GetBucketExpires(bucket_key)
  • GetBucketRemaining(bucket_key)
  • SetBucket(bucket_name, bucket_value, expires_in)

  • Examples of keyed buckets:

  • Bot bucket_key of test: bot-1-test
  • Client bucket_key of test: character-2-test
  • NPC bucket_key of test: npc-3-test

Getting Bucket Key

  • You can use GetBucketKey() on any Mob type to return their bucket key if you wish to use their key for something else.

Expiration Examples

  • Below in this Lua example we will count the number of times a player has talked to an NPC, first by checking if we've got a bucket set at all, if not we will set an expiration time on it. Each time we call set_data, it will not over-ride the original expiration time unless we pass a new time parameter
function event_say(e)
    if (e.message:findi("hail")) then
        -- Set unique key for the bucket
        local key = e.other:GetCleanName() .. "_times_talked";

        -- If the bucket is empty, we need to set it
        -- The first time we will set an expiration on this (86400 seconds)
        if (eq.get_data(key) == "") then
            eq.set_data(key, '1', 86400);

        local times_talked = tonumber(eq.get_data(key));

        e.self:Say("You know... You've talked to me " .. times_talked .. " time(s) today, get a life will ya!");

        -- Increment times talked
        eq.set_data(key, tostring(times_talked + 1));




Acceptable Time Formats

We have the ability to use time shorthands if need-be, the following are acceptable time inputs

Input Time Result
15s 15 seconds
s15 15 seconds
60m 60 minutes
7d 7 days
1y 1 year
600 600 seconds

Perl Expiration

  • To set an expiration time in Perl, very similarly to the Lua example above, you would simply call your set_data() or SetBucket() function with an expiration flag as your 3rd parameter like so

Manual Keying

my $bucket_key = $client->CharacterID() . "-example-flag";
quest::set_data($bucket_key, "some_value", 3600); # 3600 seconds = 1 hour (Expire in 1 hour)

Automatic Keying

my $bucket_key = "example-flag";
$client->SetBucket($bucket_key, "some_value", 3600); # 3600 seconds = 1 hour (Expire in 1 hour)


  • Below are some simple benchmarks used to calculate performance. While even these numbers could be greatly optimized yet, these are plenty good for most use cases that server operators need. If you need even faster temporary data storage within the context of a zone, I would suggest using [[Entity Variables]] as they operate purely in memory


    use Time::HiRes;
    my $start = [ Time::HiRes::gettimeofday() ];

    if ($text=~/random-write/i) {
        my $iterations = 1000;
        my $key_range = 100;
        quest::debug("Testing random-write... Iterations: (" . plugin::commify($iterations) . ") Key Range: " . $key_range);
        for ($i = 0; $i < $iterations; $i++) {
            quest::set_data("key_" . int(rand($key_range)), &generate_random_string(100));
    } elsif ($text=~/sequential-write/i) {
        my $iterations = 1000;
        quest::debug("Testing sequential-write... Iterations: (" . plugin::commify($iterations) . ")");
        for ($i = 0; $i < $iterations; $i++) {
            quest::set_data("key_" . $i, &generate_random_string(100), 15);
    } elsif ($text=~/sequential-read/i) {
        my $iterations = 1000;
        quest::debug("Testing sequential-read... Iterations: (" . plugin::commify($iterations) . ")");
        for ($i = 0; $i < $iterations; $i++) {
            $data = quest::get_data("key_" . $i);
            # if ($data ne "") {
            #     quest::say("Data for $i : $data");
            # }
    } elsif ($text=~/random-read/i) {
        my $iterations = 1000;
        quest::debug("Testing random-read... Iterations: (" . plugin::commify($iterations) . ")");
        for ($i = 0; $i < $iterations; $i++) {
            $data = quest::get_data("key_" . int(rand($iterations)));

    my $elapsed = Time::HiRes::tv_interval($start);
    quest::debug("Operation took: " . $elapsed);

Practical Example

In South Qeynos, you will find an NPC named Vicus Nonad. Vicus is a tax collector, but because of his terrible cold, he needs a player to help make the rounds to collect taxes. The early implementation of this quest script utilized quest globals, and below is an example of replacing the quest global functionality with Data Buckets.

    #:: Set a timer "cough" to repeat every 350 seconds (5 min 50 sec)
    quest::settimer("cough", 350);

    #:: Match the "cough" timer
    if ($timer eq "cough") {
        quest::emote("coughs and wheezes.");

    if ($text=~/hail/i) {
        my $cough_link = quest::saylink("cough");
        quest::say("Greetings, $name.My name is Vicus Nonad. <cough>I am the official tax collector for the fine city of Qeynos. <cough>I serve the will of Antonius Bayle, our glorious leader. <cough><cough>Please excuse my [$cough_link]. <cough>");
    } elsif ($text=~/cough/i) {
        my $help_link = quest::saylink("help with today's collections");
        quest::say("Oh, <cough> I am sorry, but it seems I have fallen a bit ill. I was caught out in the rain the other day and my chills have gotten the best of me. <cough> If only someone would [$help_link].. <cough>");
    } elsif ($text=~/help with today's collections/i) {
        #:: Data bucket to verify quest has been started appropriately
        my $bucket_key = $client->CharacterID() . "-tax-collection";

        #:: Set a data bucket, quest started
        quest::set_data($bucket_key, 1);

        my $list_link = quest::saylink("link");
        quest::say("Oh thank <cough> you so <cough> <cough> much <cough>.. Here is the official collection box. Please collect from each merchant on the <cough> [$list_link]. Then bring me back the combined total of all your collections.");
        #:: Give a 17012 - Tax Collection Box
    } elsif ($text=~/list/i) {
        quest::say("Oh. <cough>I am sorry.. I forgot to give it to you. Here you go. Be sure to give that back when your job is finished. <cough>");
        #:: Give a 18009 - List of Debtors

    #:: Match a 18009 - List of Debtors and 13181 - Full Tax Collection Box
    if (plugin::takeItems(13181 => 1, 18009 => 1)) {
        quest::say("<cough> Great! Thank you so much. Here is a small gratuity for a job well done. Thank you again. <cough> Antonius Bayle and the People of Qeynos appreciate all yo have done.");
        #:: Delete the data bucket
        $bucket_key = $client->CharacterID() . "-tax-collection";
        #:: Give a random reward: 13053 - Brass Ring, 10010 - Copper Amulet, 10018 - Hematite, 10017 - Turquoise
        quest::summonitem(quest::ChooseRandom(13053, 10010, 10018, 10017));
        #:: Ding!
        #:: Set factions
        quest::faction(219,10);     #:: + Antonius Bayle
        quest::faction(262,4);      #:: + Guards of Qeynos
        quest::faction(304,-5);     #:: - Ring of Scale
        quest::faction(273,-10);    #:: - Kane Bayle
        quest::faction(291,10);     #:: + Merchants of Qeynos
        #:: Grant a small amount of experience
        #:: Create a hash for storing cash - 200 to 300cp
        my %cash = plugin::RandomCash(200,300);
        #:: Grant a random cash reward
    #:: Match a 13181 - Full Tax Collection Box
    elsif (plugin::takeItems(13181 => 1)) {
        quest::say("Very good <cough> work. But I need both the full tax collection box and the list of debtors. You did get the [" . quest::saylink("list") . "] from me before you left, right? <cough>");
        #:: Return a 13181 - Full Tax Collection Box
    #:: Match a 18009 - List of Debtors
    elsif (plugin::takeItems(18009 => 1)) {
        quest::say("Very good <cough> work. But I need both the full tax collection box and the list of debtors. You did get the [" . quest::saylink("list") . "] from me before you left, right? <cough>");
    #:: Return unused items

Above, we see the implementation of the Data Bucket Key. This replaces the use of quest::setglobal. In the Database, we see the corresponding key:

Note that we clean up the key upon handing in the requisite tax money and list with the following (line 41 above):

Manual Keying

my $bucket_key = $client->CharacterID() . "-tax-collection";

Automatic Keying

my $bucket_key = "tax-collection";

The original portion of the script for the Quest Global would have been as follows:

#:: Match for "help", case insensitive
elsif ($text=~/help/i) {
    quest::say("Oh thank <cough> you so <cough> <cough> much <cough>..  Here is the official collection box.    Please collect from each merchant on the <cough> [list].    Then bring me back the combined total of all your collections.");
    #:: Give a 17012 - Tax Collection Box
    #:: Set the qglobal "tax_collection", to a value of "0", for all NPCs and Zones, and last forever
    quest::setglobal("tax_collection", 0, 5, "F");

While this implementation may seem to be easier, it should be noted that on a server with many players, running a query through a hash of globals for each event that triggers a look up can cause serious performance issues. Imagine a hash being created for every global qglobal entry (the "5" in the script above), for EVERY event trigger!

Our intrepid adventurer is required to do this quest in order, by speaking with Vicus prior to the merchants in Qeynos who have to pay taxes. To enforce this, we verify that the user has the appropriate data bucket key set (line 9) before offering the dialogue that results in the tax payment:

    if ($text=~/hail/i) {
        quest::say("Hail, $name. What brings you to the Tin Soldier? We have the finest in previously owned weapons and armor. Feel free to browse.");
    } elsif ($text=~/tax collection/i) {
        #:: Data bucket to verify quest has been started appropriately
        my $bucket_key = $client->CharacterID() . "-tax-collection";
        #:: Match if the key exists
        if (quest::get_data($bucket_key)) {
            quest::say("Here are the taxes, $name. Boy, you call the guards and they take their time to show up but be a few days late on your taxes and they send the goons after you. Sheesh!");
            #:: Give a 13171 - Sedder's Tax Payment
            #:: Set faction
            quest::faction(291,-1); #:: - Merchants of Qeynos

    #:: Return unused items