Files
Anphillia_PRC8/_module/nss/pinv_inc.nss
Jaysyn904 28cdb617b3 Initial commit
Adding all of the current content for Anphillia Unlimited.
2024-01-04 07:49:38 -05:00

428 lines
16 KiB
Plaintext

//void main(){}
//::///////////////////////////////////////////////
//:: Persistent Inventory Include Script. version 1.0
//:: 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.
notes:
you can also add full "containers" into a chest. But this will cause a small hassle
caused by the engine's way of handling inventories. Once you placed your container and
close the chest, then its inventory will swap over to the chest (you will not loose anything).
after you re-open, all the items from the container will now be part of the chests inventory and
the container will be empty. it's like you would pour the container content into the chest...
i could do a workaround, but it would involve some complex scripting and a more complex database
structure. of course, it would be slower... so i think i won't do it because it's not worth the hassle
and no big deal.
btw, it is now possible to store ANY item, no more restrictions thanks to v1.30 patch
"locking" a chest the way i do it disables the "close" button. you can still easily close
the container just by walking a bit away from it. this is a small hassle, but a container can't
be opened by two people at once now.
--- exploit fix ----
action-spam exploit which works like this:
PC moves some meters away from the chest and starts left-clicking it in a rapid manner while the PC
starts to move towards it. After the arrival, his action queue will spam the chest:
OnUse/OnOpen/OnClose fire in a very rapid manner and the chest will get "stuck open" most of the time,
not firing OnClose/OnOpen anymore.
this will not only spork many scripts, it will also render this chest unusable. this can lead to bad stuff.
i worked heavily on a fix and got something ready. it's not a clever one, it's a brute force one.
but it works 100% of the time.
one small hassle:
your chest/placeable needs to be present on the palette.
you have to create it beforehand with all scripts and stuff, then dropping it to your map.
it won't work if you modified your chest after you dropped it to the map.
changelog:
v 1.1
- added dynamic watchdog to scan for "Stuck Open" chests (applied to pcnt_open)
- added various exploit fixes (applied to pcnt_open, pcnt_close)
- fixed two bugs (thanks for benton for the headsup):
1. DestroyInventory() did not delete cache
2. forgot to use correct database name in case cache was deactivated
v 1.0 (first final)
- added a floating text message "Please walk away to close container" while the player
opens a persistent container.
- fixed container "lock" mechanism. it should now correctly stop other players
from opening an already busy container.
v 0.6
- 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
const 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)
const string SECURE_DELIMITER = "#";
// everything not in here gets considered an illegal character
// - mixed up for additional security
const string HASH_INDEX = "#i!j$k%l{&M/n(o)p=q?r^Xs`Tu'v]AwBxCyDzE1F2-G3t;4I}5Y:J6_K7+Z[Lm9N\ l0kOjPhQ,gRfSeHdU8cVbWa.";
const int HASH_PRIME = 3021377;
// database filename
// change to your own needs
const string DB_NAME = "DB_CONTAINER";
// --------------------------- 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)
{
if (USE_CACHE) DeleteLocalString(GetModule(),"DB_INV_CACHE"+IntToString(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;
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_NAME,IntToString(nID), sItems);
return 0;
}
else
return -1;
}
// converts item into string (serialize)
// resref + stack + charges + 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)
{
// 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));
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);
}