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.

Functions

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

Perl

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

$mob->DeleteBucket(bucket_key);
$mob->GetBucket(bucket_key);
$mob->SetBucket(bucket_key);

Lua

eq.delete_data(bucket_key);
eq.get_data(bucket_key);
eq.set_data(bucket_key, bucket_value, expires_in);

mob:DeleteBucket(bucket_key);
mob:GetBucket(bucket_key);
mob:SetBucket(bucket_key, bucket_value, expires_in);

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

Perl

quest::get_data_expires(bucket_key);
quest::get_data_remaining(bucket_key);

$mob->GetBucketExpires(bucket_key);
$mob->GetBucketRemaining(bucket_key);

Lua

eq.get_data_expires(bucket_key);
eq.get_data_remaining(bucket_key);

mob:GetBucketExpires(bucket_key);
mob:GetBucketRemaining(bucket_key);

Storage

  • 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)
character_id Character Identifier
npc_id NPC Identifier
bot_id Bot Identifier
  • 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

Perl

  • 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!");
}

Lua

  • 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") .. "'");

    eq.delete_data("lua_test_key");

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

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)

sub EVENT_CLICKDOOR {
    quest::say($doorid);
    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);
    }
}

Result

Database Result

By NPC

Manual Keying

sub EVENT_DEATH_COMPLETE {
    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

sub EVENT_DEATH_COMPLETE {
    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!");
}

Result

Database

Automatically Keying Buckets

  • Automatic keying is a Mob only thing, you will still have to manually key anything else.
  • These methods automatically grab set columns based on Mob type.
  • Bots use bot_id
  • Clients use character_id
  • NPCs use npc_id

  • DeleteBucket(bucket_key)

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

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);
        end

        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));
    end

end

Result

Database

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)

Benchmarks

  • 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

image

sub EVENT_SAY {
    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.

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

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

sub EVENT_SAY {
    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
        quest::summonitem(17012);
    } 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
        quest::summonitem(18009);
    }
}

sub EVENT_ITEM {
    #:: 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";
        quest::delete_data($bucket_key);
        #:: Give a random reward: 13053 - Brass Ring, 10010 - Copper Amulet, 10018 - Hematite, 10017 - Turquoise
        quest::summonitem(quest::ChooseRandom(13053, 10010, 10018, 10017));
        #:: Ding!
        quest::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
        quest::exp(500);
        #:: Create a hash for storing cash - 200 to 300cp
        my %cash = plugin::RandomCash(200,300);
        #:: Grant a random cash reward
        quest::givecash($cash{copper},$cash{silver},$cash{gold},$cash{platinum});
    }
    #:: 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
        quest::summonitem(13181);
    }
    #:: 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
    plugin::returnUnusedItems();
}

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";
quest::delete_data($bucket_key);

Automatic Keying

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

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
    quest::summonitem(17012);
    #:: 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:

sub EVENT_SAY {
    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) {
        #:: Match if the key exists
        if ($client->GetBucket("tax-collection") {
            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
            quest::summonitem(13171);
            #:: Set faction
            quest::faction(291,-1); #:: - Merchants of Qeynos
        }
    }
}

sub EVENT_ITEM {
    #:: Return unused items
    plugin::returnUnusedItems();
}

Scaling Heroic Stat Bonuses via Data Buckets

The rule Character, HeroicStatsUseDataBucketsToScale Allows scaling the benefits a Player or Bot receives from Heroic Stats using Data Buckets. This works with other multipliers, as long as a valid data bucket is applied to the character.

Below are valid keys that can be used, and the benefit given if keyed with a value of "1.00" or "1".

Bucket Key "character-id-KEY" Description assuming Bucket Value of "1.00"
HSTR-MeleeDamage 1 melee damage per 10 Heroic Strength.
HSTR-ShieldAC 1 Shield AC per 10 Heroic Strength.
HSTR-MaxEndurance 2.5 Base Endurance per every 1 Heroic Strength.
HSTR-EnduranceRegen" 0.05 Endurance Regen per 10 Heroic Strength.
HSTA-MaxHP 10 HP per 1 Heroic Stamina.
HSTA-HPRegen 0.5 HP Regen per 10 Heroic Stamina.
HSTR-MaxEndurance 2.5 Base Endurance per every 1 Heroic Stamina.
HSTA-EnduranceRegen 0.05 Endurance Regen per 10 Heroic Stamina.
HAGI-Avoidance 1 Avoidance per 10 Heroic Agility.
HAGI-MaxEndurance 2.5 Base Endurance per every 1 Heroic Agility.
HAGI-EnduranceRegen 0.05 Endurance Regen per 10 Heroic Agility.
HDEX-RangedDamage 1 Ranged Damage per 10 Heroic Dexterity.
HDEX-MaxEndurance 2.5 Base Endurance per every 1 Heroic Dexterity.
HDEX-EnduranceRegen 0.05 Endurance Regen per 10 Heroic Dexterity.
HINT-SpellDmg 1 Spell Damage per 1 Heroic Intelligence.
HINT-MaxMana 250 Base Mana per every 25 Heroic Intelligence.
HINT-ManaRegen 1 Mana Regen/Regen Cap per 25 Heroic Intelligence.
HWIS-HealAmt 1 Heal Amount per 1 Heroic Wisdom.
HWIS-MaxMana 250 Base Mana per every 25 Heroic Wisdom.
HWIS-ManaRegen 1 Mana Regen/Regen Cap per 25 Heroic Wisdom.

Practical Example

Say we want a player to receive 10 melee damage per 10 Heroic Strength, we would set a bucket as follows:

my $bucket_key = "HSTR-MeleeDamage";
$client->SetBucket($bucket_key, 10.00);