Author Topic: Database Driven Loot System  (Read 317 times)

Legacy_Kato -

  • Hero Member
  • *****
  • Posts: 747
  • Karma: +0/-0
Database Driven Loot System
« on: June 08, 2011, 09:22:40 pm »


               Hi,

I've heard a few times about database driven loot systems, and since I'm currently using the "classical" system with hidden chests containing low, med and high quality items + randomized index + CopyItem(), I would very much like to go for the DB way, since the chests contain more and more items, enlarging the module a lot, not mentioning that each contained item has both an object and an int variable(for index) stored on the chest. With a few items, it was fine, but with hundreds of them, ouch...

I would like to store the item resrefs in a MySQL table and randomize reads, yet it implies using CreateItemOnObject() instead of CopyObject(), and I was told this could create some serious lag. Am I looking in the wrong direction?

Thank you!
               
               

               
            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
Database Driven Loot System
« Reply #1 on: June 08, 2011, 11:29:32 pm »


               The difference between the two isn't so big as to preclude using it, by any means - we do this as well. Let me know if you need help figuring out the setup.

Funky
               
               

               
            

Legacy_Axe_Murderer

  • Full Member
  • ***
  • Posts: 199
  • Karma: +0/-0
Database Driven Loot System
« Reply #2 on: June 09, 2011, 06:11:20 am »


               Unless you plan to generate lots of items, all at once, quite often, I wouldn't worry about the lag differences between those two functions.
               
               

               
            

Legacy_Kato -

  • Hero Member
  • *****
  • Posts: 747
  • Karma: +0/-0
Database Driven Loot System
« Reply #3 on: June 09, 2011, 06:16:02 pm »


               Many thanks, gentlemen, I can't wait to delete those chests, then! Hey Funky, without wanting to abuse, I would indeed appreciate some clues regarding the initial setup. For instance, knowing that some creatures(but not all) spawn items from different categories(crafting, gems etc...) upon dying, would it be wiser to use one table for each category or rather simply add a category column in one big resref table?

Again, thank you so much!

Kato
               
               

               


                     Modifié par Kato_Yang, 09 juin 2011 - 05:51 .
                     
                  


            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
Database Driven Loot System
« Reply #4 on: June 09, 2011, 08:24:29 pm »


               Well...it comes down to shaping the tables to what you want to do. Ours are a mess - we only use database-driven stuff for randomization, though we've discussed making the script-driven stuff we have into a database-driven setup. For us, we have different categories of loot (common, uncommon, rare, ultrarare, and beyond ultrarare), which have different chances of spawning depending on the area the loot is popping in. Thus, we wouldn't be able to assign a single weighting to each item. Instead, we would probably want to leave the initial bit in script, and simply create a table for each rarity type. Within those tables, we could then add different weights to each item (suppose we wanted, for example, any random rare weapon to spawn at the same frequency as any other rare, instead of giving each individual item a full weighting

We might then assign a standard item weight of 50, and only give weapons a weight of one.

For our random item property system, we divide the properties into categories, giving each category a different weighting, and each property in the category an equal portion of its weighting. This is going to sound more complex than it really is. It allows us to dynamically update weighting when properties are added, or when we want to tweak frequencies. We use the following function to recalibrate them while the server is running:


void SetRandomIPWeights(object oPC) {
    int nIP, nCount;
    string sSQL = "SELECT rp_type, count(*) FROM rip_props WHERE rp_rarity <> 0 GROUP BY rp_type";
    SQLExecDirect(sSQL);
    while (SQLFetch() != SQL_ERROR) {
        nIP = StringToInt(SQLGetData(1));
        nCount = StringToInt(SQLGetData(2));
        SetLocalInt(oPC, "Weight"+IntToString(nIP), nCount);
    }
    SQLExecDirect("UPDATE rip_props SET rp_weight = 1500 where rp_rarity = 5");
    SQLExecDirect("UPDATE rip_props SET rp_weight = 3800 where rp_rarity = 4");
    SQLExecDirect("UPDATE rip_props SET rp_weight = 9600 where rp_rarity = 3");
    SQLExecDirect("UPDATE rip_props SET rp_weight = 24200 where rp_rarity = 2");
    SQLExecDirect("UPDATE rip_props SET rp_weight = 60900 where rp_rarity = 1");
    SQLExecDirect("UPDATE rip_props SET rp_weight = 0 where rp_rarity = 0");

    for (nIP = 0;nIP < 256;nIP++) {
        if (nCount = GetLocalInt(oPC, "Weight"+IntToString(nIP))) {
            DeleteLocalInt(oPC, "Weight"+IntToString(nIP));
            SQLExecDirect("UPDATE rip_props SET rp_weight = rp_weight/" + IntToString(nCount) + " WHERE rp_type = " + IntToString(nIP));
        }
    }
    SQLExecDirect("UPDATE rip_props SET rp_weight = rp_weight*2 where rp_type = 12 or rp_type = 202");
}

You will likely be able to do something much simpler. First, though, you need to determine PRECISELY how you want your system to hand out loot. For example:
-Do you want any piece of random loot to be able to be found in any area?
-If not, do you want more powerful pieces of loot seperated from weaker, or would you prefer some other kind of split, perhaps just designating one or more areas or types of areas it could be found in?
-If each random piece of loot could only be found in one area, for example, you could simply mark that area's identifier (resref, or local int, or whatever you choos) in a column, and do select for pieces of loot in that column, randomly selecting one. If you wanted to do area types, you could try a bitflag approach.

In any event, how you set up your tables is going to depend GREATLY on what it is you want your system to do. I'm happy to help, but you'll need to be able to describe its ideal operation in a fair amount of detail.

Funky
               
               

               
            

Legacy_Kato -

  • Hero Member
  • *****
  • Posts: 747
  • Karma: +0/-0
Database Driven Loot System
« Reply #5 on: June 09, 2011, 11:03:22 pm »


               Okay, certainly. Currently, my only criterion is the creature dropping the item, or the placeable being opened. I have no area-specific items for now. So it goes like this, for instance: A specific creature could drop one item(with a % chance of no item) from the gems category, a boss would drop one "set" item from the set items category, a nature placeable(a shrub, in this case) would create an herb or fruit from the fruits category, and so on... of course, each category holds some differently weighted items, and although some creatures will drop specific category items, most will totally randomize between categories. For item properties, I'm making a note of the concept of differently weighted ip categories you describe, yet for now I only need to randomize material and/or quality, both being set as variables on the item(to leave more room for upgrades, since I don't trust raising the max item props constant in x2_inc_itemprop).

So this is it, I hope I did not forget anything... Except to thank you a lot for taking the time to read this.

Kato
               
               

               


                     Modifié par Kato_Yang, 09 juin 2011 - 11:25 .
                     
                  


            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
Database Driven Loot System
« Reply #6 on: June 10, 2011, 03:10:58 am »


               My inclination there would be to create a single table with resref, weight, and category columns, at a minimum. I would handle category selection in nwscript, as well as the chance of getting nothing.

Here's a sample of our RandomItemProperty Properties table.

mysql> select * from rip_props limit 5;
+-------+---------+--------+---------+---------+----------+--------+--------+------------+-----------+---------------+-------------+-----------+----------------+
| rp_id | rp_type | rp_sub | rp_cval | rp_pval | rp_range | rp_min | rp_max | rp_inverse | rp_rarity | rp_allow_item | rp_block_ip | rp_weight | rp_std_dev_cap |
+-------+---------+--------+---------+---------+----------+--------+--------+------------+-----------+---------------+-------------+-----------+----------------+
|     1 |       0 |      0 |      -1 |       0 | (x*2)+4  |      1 |     16 |          0 |         1 |    1073872895 | x           |     10150 |              2 |
|     2 |       0 |      1 |      -1 |       0 | (x*2)+4  |      1 |     16 |          0 |         1 |    1073872895 | x           |     10150 |              2 |
|     3 |       0 |      2 |      -1 |       0 | (x*2)+4  |      1 |     16 |          0 |         1 |    1073872895 | x           |     10150 |              2 |
|     4 |       0 |      3 |      -1 |       0 | (x*2)+4  |      1 |     16 |          0 |         1 |    1073872895 | x           |     10150 |              2 |
|     5 |       0 |      4 |      -1 |       0 | (x*2)+4  |      1 |     16 |          0 |         1 |    1073872895 | x           |     10150 |              2 |
+-------+---------+--------+---------+---------+----------+--------+--------+------------+-----------+---------------+-------------+-----------+----------------+
5 rows in set (0.00 sec)


I would still include an auto-incrementing column, which is what rp_id is. Past that, I suspect you would want resref, category, and weight. You might also want a meta-weight column of some sort, depending, but you could easily handle that in nwscript.

What do I mean by meta-weight? Something that treats the weight of an item as the relative rarity, and then uses it to assign a relative weighting number based on the total number of items, or the total number of items in that items category, depending on your intended use. It would operate something like this:


void SetAugmentWeights (object oPC) {
    SQLExecDirect("SELECT FLOOR(ra_rarity / 1000) AS a, COUNT(DISTINCT ra_rarity % 1000) AS b " +
                  "FROM rip_augments GROUP BY a HAVING b <> 1");

    if (SQLFetch() != SQL_ERROR) {
        do {
            WriteTimestampedLogEntry("INVALID RARITY SUBclass : " + SQLGetData(1) + " : " + SQLGetData(2));
        } while (SQLFetch() != SQL_ERROR);

        return;
    }

    SQLExecDirect("SELECT a.ra_rarity, COUNT(*), "                                              +
                  "  (SELECT COUNT(DISTINCT b.ra_rarity) FROM rip_augments AS b WHERE "         +
                  "     b.ra_rarity % 1000 = 0 AND "                                            +
                  "     FLOOR(b.ra_rarity / 1000000) = FLOOR(a.ra_rarity / 1000000)) AS share " +
                  "FROM rip_augments AS a GROUP BY a.ra_rarity ORDER BY a.ra_rarity DESC");

    int nLastRarity = -1, nRemaining = 0;
    string sSQL = "";

    while (SQLFetch() != SQL_ERROR) {
        int nRandP      = StringToInt(SQLGetData(1));
        int nCount      = StringToInt(SQLGetData(2));
        int nProportion = nRandP % 1000;
        int nRarity     = nRandP / 1000000;

        if (nRarity != nLastRarity) {
            nLastRarity = nRarity;
            nRemaining  = 1000;
        }

        int nWeight = GetRandomIPRarityclassWeight(nRarity);

        if (nProportion != 0) {
            nWeight     = (nWeight * nProportion) / 1000;
            nRemaining -= nProportion;
        } else {
            int nShare = StringToInt(SQLGetData(3));
            nWeight    = (nWeight * nRemaining) / (nShare * 1000);
        }

        sSQL += " WHEN " + IntToString(nRandP) + " THEN " + IntToString(nWeight / nCount);
    }

    if (sSQL != "")
        SQLExecDirect("UPDATE rip_augments SET ra_weight = (CASE ra_rarity " + sSQL + " ELSE 0 END)");
}

It's a little more complex than if you don't use wieghting at all. Under such a system, you would then pick an random (weighted) item (or augment, in this case), like so:


struct Augment GetRandomAugment(string sConditions) {
    string sSQL;
    struct Augment ret;

    //pull up all augments that are of acceptable IP and item type, totalling weight
    sSQL = "SELECT @ra_random := FLOOR(RAND() * SUM(ra_weight)) " +
           "FROM rip_augments WHERE " + sConditions;
    SQLExecDirect(sSQL);

    //select one of those augments by weight
    sSQL = "SELECT ra_id,ra_name,ra_block_aug,ra_allow_item,ra_notes,ra_affix,ra_appearance,ra_rarity FROM rip_augments WHERE (" + sConditions +
           ") AND (@ra_random := @ra_random - ra_weight) < 0 LIMIT 1";
    SQLExecDirect(sSQL);

    while (SQLFetch() != SQL_ERROR) {
        ret.id          = StringToInt(SQLGetData(1));
        ret.name        = SQLDecodeSpecialChars(SQLGetData(2));
        ret.blockaug    = SQLGetData(3);
        ret.allowitem   = StringToInt(SQLGetData(4));
        ret.notes       = SQLDecodeSpecialChars(SQLGetData(5));
        ret.affix       = StringToInt(SQLGetData(6));
        ret.appear      = StringToInt(SQLGetData(7));
        ret.rarity      = StringToInt(SQLGetData(8));
    }
    return ret;
}

Whether or not you want to deal with weighting at all is going to depend on how much you care about fine-tuning item drops. If you don't want to, there may be very little reason for you to go database-driven at all. MySQL is fantastic for dynamically recalculating weighting when you add items, but if you want them all to have roughly the same drops, it may not matter all that much. Or, you can achieve rough weighting using varying levels of brackets in case or if statements. Something like this:

Funky
               
               

               
            

Legacy_Kato -

  • Hero Member
  • *****
  • Posts: 747
  • Karma: +0/-0
Database Driven Loot System
« Reply #7 on: June 10, 2011, 03:44:49 am »


               Wow! I did not expect to bother you that much... This certainly answers all my questions since yes, I'll deal with weightings. Thank you again for your time and help, Funky.

Kato