//void main(){}
//::///////////////////////////////////////////////
//:: Persistent Inventory Include Script. version 0.6 (Beta)
//:: Copyright (c) 2001 Bioware Corp.
//:://////////////////////////////////////////////
/*

  this script helps you to store/retrieve inventory to/from the bioware database

  it can serialize several items into a single padded string. you can then retrieve
  this string and rebuild those items on any valid object inventory.

  persistent containers are only one usage example.

changelog:

  - charged items get now handled properly thanks to GetItemCharges/SetItemCharges

  - increased item stack size to 5. you can now store any amount of gold.
    one small restriction: you can't move more then 50000 gold at once. but you
    can store any number of gold stacks. this takes container capacity into account.
    e.g. container with max capacity of 10 can hold 500.000 gold (10 stacks of 50.000)

  - added blocking
    block specifc items by item type or tag filter via PINV_BlockItemByType(),PINV_BlockItemByFilter()
    see PINV_open for usage example

  - added auto caching.
    container will only update to database if the inventory got manipulated.
    reading cache will only read the inventory one time at first usage of PINV_RetrieveInventory().
    storing of additional items will not issue a new read, just a single db write + readcache update.
    this is much faster now...

  - added hashing
    IDs now get hashed. no more limits on area TAG length while using unique IDs
    this also grants additional security.

  - secure delimiter added
    IDs that contain a secure delimiter character will get rejected
    this should prevent any form of hijacking

  - container now retains his sorting order through the use of LIFO (last in first out)
    for inventory creation/storing.

  - a closed chest is ALWAYS completely empty. this preserves lots of RAM on large modules

FAQ (under construction)

When do i use TAG IDs ? (ReflexSafe = 0)
 if you have a moving container (like a creature) OR if you want
 to share the same inventory between different containers.

When do i use unique IDs ? (ReflexSafe = 1)
 use this kind of ID if your container is stationary. you won't
 have any tag dependency this way.

When do i use player IDs ? (ReflexSafe = 2)
 use this kind of ID if you want a player vault.
 a container with this setting will retrieve inventory based
 on the using player. this is the classical PC vault thingy...

*/

//:://////////////////////////////////////////////
//:: Created By:  Knat
//:: Created On:  28.04.03
//:: Last Change: 14.06.03
//:://////////////////////////////////////////////

// --------------------------- GLOBALS -----------------------------------
//
// feel free to adjust them to fit your needs

// caching increases memory usage..
// if you don't wanna use caching, set this to FALSE
int USE_CACHE = TRUE;

// needed to prevent DB hijacking
// this char is forbidden in PC names
// PCs with this delimiter in their name won't be able
// to manipulate player vaults (ID_FLAG = 2)
string SECURE_DELIMITER = "#";

// everything not in here gets considered an illegal character
// - mixed up for additional security
string HASH_INDEX = "#i!j$k%l{&M/n(o)p=q?r^Xs`Tu'v]AwBxCyDz" +
                    "E1F2-G3t;4I}5Y:J6_K7+Z[Lm9N\ l0kOjPhQ,gRfSeHdU8cVbWa.";

int HASH_PRIME = 3021377;

// database filename
// change to your own needs
string DB_NAME = "DB_CONTAINER_"+GetTag(GetModule());

// --------------------------- IMPLEMENTATION ----------------------------

// converts item into string (serialize)
// padding gets used to make parsing more easy, should be faster then tokenizing it...
// this also gives us a fixed length. stringsize / 24 = number of items.
// makes cycling over each item easy and very fast. all items start at position (itemindex*20)
// e.g. 5th item in container starts at stringposition 110
// this way, accessing specific items is fast and does not need any searching/looping
string ItemToPaddedString(object oItem);

// converts string to item (de-serialize)
// oTarget = item receiver
object PaddedStringToItem(string sItem, object oTarget = OBJECT_SELF);

// return database ID from oTarget
// nIDType = 0
//   non unique container ID. it will just use the TAG as container index
//   multiple objects can share the same inventory this way
// nIDType = 1
//   unique object ID. area-tag + location used as object index. each object gets
//   his own unique inventory space, tag is irrelevant.
//   object must be stationary !!!!
// nIDType = 2
//   ID based on GetPCPlayerName() and GetName()
//   object inventory depends on opener
//   this is useful for player vaults
int PINV_GetID(object oTarget, int nIDType = 0);

// destroy persistent inventory
void PINV_DestroyInventory(int nID);

// store inventory to database
// only needs a single database call to store all items
// nID = database ID - you can retrieve that ID from any inventory object via PINV_GetID()
// oTarget = inventory object (e.g.: creature, placeable, merchant, player)
// oFail = items that can't be stored are given back to the oFail object
//         (usually a player, but can be anything with an inventory)
//         if this object is invalid = item gets destroyed
// nMaxCapacity = maximum inventory capacity
//
// function returns -1 if it can't generate an ID. this usually only happens
// if the target object is a player that has invalid characters in his name
// this should pretty much eliminate any DB hijacking
int PINV_StoreInventory(int nID, object oTarget = OBJECT_SELF, object oFail = OBJECT_INVALID, int nMaxCapacity = 50);

// retrieve inventory from database
// this functions reads an inventory string from the database and re-creates
// items on the specified target.
// only needs a single database call to retrieve all items
//
// nID = database ID - you can retrieve that ID from any inventory object via PINV_GetID()
// oTarget = inventory object (e.g.: creature, placeable, merchant, player)
//
// function returns -1 if it can't generate an ID. this usually only happens
// if the target object is a player that has invalid characters in his name
// this should pretty much eliminate any DB hijacking
int PINV_RetrieveInventory(int nID, object oTarget = OBJECT_SELF);

// forces oTarget to block items of nBaseItemType (BASE_ITEM_*)
void PINV_BlockItemByType(int nBaseItemType, object oTarget = OBJECT_SELF);

// forces oTarget to block items containing sTagFilter in their TAG
// e.g. "**NODROP**" will block items with NODROP anywhere in their tag
void PINV_BlockItemByFilter(string sTagFilter, object oTarget = OBJECT_SELF);

// placeholder
// this function will create an inventory string from a 2DA file
// will be very useful later for dynamic inventory management.
string PINV_GetInventoryFrom2DA(string s2DAFile, int nFirstRow, int nLastRow)
{
  return "";
}

/* ----------------------------------------------------------------------- */

// simple hash
// returns -1 if string contains illegal character
int hash(string sData)
{
  int nLen = GetStringLength(sData);
  int i, nHash, nChar;
  for(i=0;i<nLen;i++)
  {
     nChar = FindSubString(HASH_INDEX, GetSubString(sData,i,1));
     if(nChar == -1) return -1;
     nHash = ((nHash<<5) ^ (nHash>>27)) ^ nChar;
  }
  return nHash % HASH_PRIME;
}

// return database ID from oTarget
int PINV_GetID(object oTarget, int nIDType = 0)
{
  string sID;
  switch(nIDType)
  {
    case 0 :
      sID = "T" + SECURE_DELIMITER + GetTag(oTarget);
      break;
    case 1 :
      sID = "U" + GetTag(GetArea(oTarget)) + SECURE_DELIMITER + IntToString(FloatToInt(GetPosition(oTarget).x * 10)) +
             IntToString(FloatToInt(GetPosition(oTarget).y * 10));
      break;
    case 2 :
      // reject player names containing secure delimiter
      if(FindSubString(GetPCPlayerName(oTarget),SECURE_DELIMITER) != -1 || FindSubString(GetName(oTarget),SECURE_DELIMITER) != -1)
        return -1;
      sID = "P" + GetPCPlayerName(oTarget) + SECURE_DELIMITER + GetName(oTarget);
      break;
  }
  return hash(sID);
}

void PINV_DestroyInventory(int nID)
{
  DeleteCampaignVariable(DB_NAME,IntToString(nID));
}

// read inventory from database with a single call
int PINV_RetrieveInventory(int nID, object oTarget = OBJECT_SELF)
{
  if(nID != -1)
  {
    string sItems;
    // database access gets reduced greatly if using the cache
    if(USE_CACHE)
    {
      sItems = GetLocalString(GetModule(),"DB_INV_CACHE"+IntToString(nID));
      // only reload on empty cache
      if(sItems == "")
      {
        sItems = GetCampaignString(DB_NAME,IntToString(nID));
        SetLocalString(GetModule(),"DB_INV_CACHE"+IntToString(nID),sItems);
      }
    }
    else
      sItems = GetCampaignString(DB_NAME,IntToString(nID));

    if(sItems != "")
    {
      // build inventory
      int i, nCount = (GetStringLength(sItems) / 24) - 1;
      SendMessageToPC(GetFirstPC(),"id:" + IntToString(nID) + " count: "+IntToString(nCount)+ " items:"+sItems);
      for(i=nCount;i>=0;i--) PaddedStringToItem(GetSubString(sItems,i*24,24),oTarget);
    }
    return 0;
  }
  else
    return -1;
}

void PINV_SendErrorAndReturnItem(string sError, object oItem, object oReceiver, object oInventoryObject=OBJECT_SELF)
{
  if(GetIsPC(oReceiver))
    SendMessageToPC(oReceiver, GetName(oInventoryObject) + ": " + sError + ". returning item ("+GetName(oItem)+")...");

  if(GetIsObjectValid(oReceiver))
    AssignCommand(oReceiver, ActionTakeItem(oItem,oInventoryObject) );
  else
    DestroyObject(oItem);
}

int PINV_StoreInventory(int nID, object oTarget = OBJECT_SELF, object oFail = OBJECT_INVALID, int nMaxCapacity = 50)
{
  if(nID != -1)
  {
    int i = 0;
    string sItems, sDummy;
    object oItem = GetFirstItemInInventory(OBJECT_SELF);

    while(oItem != OBJECT_INVALID)
    {
      if(i < nMaxCapacity) // enough space left ?
      {
        // check blocking filters
        if( GetLocalInt(oTarget,"BLOCK_TYPE#"+IntToString(GetBaseItemType(oItem))) ||
            TestStringAgainstPattern( GetLocalString(oTarget,"BLOCK_TAG"), GetTag(oItem)))
        {
          PINV_SendErrorAndReturnItem("It's not allowed to store this", oItem, oFail);
        }
        else
        {
          // check for large stacks
          if(GetNumStackedItems(oItem) > 50000)
          {
            PINV_SendErrorAndReturnItem("It's not allowed to store stacks of gold > 50000. Store your gold in several stacks of 50000 or less", oItem, oFail);
          }
          else
          {
            // serialize item
            sDummy = ItemToPaddedString(oItem);
            if(sDummy == "")
            {
              // invalid item
              PINV_SendErrorAndReturnItem("Can't store this item because of empty tag AND resref", oItem, oFail);
            }
            else
            {
              // item passed all checks and gets stored
              // add item to string, inc item counter and destroy item
              i++; sItems += sDummy; DestroyObject(oItem);
            }
          }
        }
      }
      else
      {
        // maximum capacity reached
        PINV_SendErrorAndReturnItem("Container maximum capacity exceeded", oItem, oFail);
      }
      oItem = GetNextItemInInventory(oTarget);
    }
    // write inventory to database with a single call
    // only update if inventory got changed
    if(USE_CACHE)
    {
      if(GetLocalString(GetModule(),"DB_INV_CACHE"+IntToString(nID)) != sItems)
      {
        SetCampaignString(DB_NAME,IntToString(nID), sItems);
        SetLocalString(GetModule(),"DB_INV_CACHE"+IntToString(nID),sItems);
      }
    }
    else
      SetCampaignString("DB_CONTAINER_"+GetTag(GetModule()),IntToString(nID), sItems);

    return 0;
  }
  else
    return -1;
}

// converts item into string (serialize)
// resref + stack + identified flag
string ItemToPaddedString(object oItem)
{
  string sItem = GetResRef(oItem);

  // blank resref ? probably merchant-bug.. , try tag instead (item resref/tag must match otherwise it won't work)
  sItem = (sItem == "") ?  GetStringLowerCase(GetTag(oItem)) : sItem;
  if(sItem == "") return "";
  // pad to 16 & add stack
  sItem = (GetStringLength(sItem) < 16) ? sItem + GetStringLeft("               ",16 - GetStringLength(sItem)) : GetStringLeft(sItem,16);
  sItem += IntToString(GetNumStackedItems(oItem));
  // pad to 21 & add number of charges
  sItem = (GetStringLength(sItem) < 21) ? sItem + GetStringLeft("            ",21 - GetStringLength(sItem)) : GetStringLeft(sItem,21);
  sItem += IntToString(GetItemCharges(oItem));
  // pad to 23 & add identified flag
  if (GetStringLength(sItem) < 23) sItem += " ";
  return sItem + IntToString(GetIdentified(oItem));
}

// converts string to item (de-serialize)
object PaddedStringToItem(string sItem, object oTarget = OBJECT_SELF)
{

  SendMessageToPC(GetFirstPC(),"Item: "+sItem);
  // check for and remove any padding
  // create item with nStackSize from DB
  int nPad = FindSubString(GetStringLeft(sItem,16)," ");
  string sRef = (nPad != -1) ? GetStringLeft(sItem,nPad) : GetStringLeft(sItem,16);
  int nStack = StringToInt(GetSubString(sItem, 16,5));
  int nCharges = StringToInt(GetSubString(sItem, 21,2));
  SendMessageToPC(GetFirstPC(),"Charges: "+IntToString(nCharges));
  object oItem = CreateItemOnObject(sRef, oTarget, nStack);
  // set identified from DB
  if(oItem != OBJECT_INVALID) SetIdentified(oItem, StringToInt(GetStringRight(sItem,1)));
  // set charges
  if(nCharges > 0) SetItemCharges(oItem, nCharges);
  return oItem;
}

void PINV_BlockItemByType(int nBaseItemType, object oTarget = OBJECT_SELF)
{
  SetLocalInt(oTarget,"BLOCK_TYPE#"+IntToString(nBaseItemType),TRUE);
}

void PINV_BlockItemByFilter(string sTagFilter, object oTarget = OBJECT_SELF)
{
  SetLocalString(oTarget,"BLOCK_TAG",sTagFilter);
}