Hey folks.
As per the thread
http://www.eqemulator.org/forums/showthread.php?t=25547 and the fix
http://www.eqemulator.org/forums/showthread.php?t=25547, there had previously been an easy to reproduce problem with users accidentally closing world containers and finding their nodrop items deleted. Wheeljack fixed this for the normal case but it still left the uncommon issue of items in world containers becoming lost if the zone crashes or shuts down in an unanticipated fashion. This can easily be tested if you open up a world container and then crash the zone by closing the server app. Upon relogging in, you will find your items missing.
Discussed my comments on the thread with Trevius in IRC and found that the proposed change to have these lost items find their owner on reuse of the container is a high risk change (duplication, no drop trade exploits) and we are not likely to fix this in the forseeable future.
Instead, handling this another way for augmentations as this is the area where users are more likely to be severely impacted. A new server rule to allow server owners to specify another on-the-character tradeskill container that can be used for augmentatin. This handles augmenting and safe-removing of augmentations. This explicitly does not allow for deletion of augments as this really requires the user to choose an augmentation slot to delete in order to be safe for the user. The rule defaults to being turned off and the augmentation pool will behave the same as it has if this is the case. If the flag is set to the item id of a tradeskill container (certain restrictions apply) it will allow auging/deauging in here without impacting the augmentation pool (for those that still wish to use that).
NOTE - merged the code paths for the augmentation pool and the tradeskill container augmentation to a common function to make sure that these do not take on separate lifes as the project continues.
Testing:
1. Validated that I could still augment slots without issue in aug pool.
2. Validated that I could still remove augments without issue in aug pool.
3. Validated that I could still delete augments without issue in aug pool..
4. Validated that if the rule is set to 0, combines in tradeskill containers are not allowed.
5. Validated that if the rule is set to a given tradeskill container (Spit, for example) that this would not allow automatic aug combination in other non specified tradeskill containers.
6. Validated that if the rule is set to a given tradeskill container you could augment with this.
7. Validated that if the rule is set to a given tradeskill container, you could remove augments with this and the appropriate de-aug juice.
8. Validated that if the rule is set to a given tradeskill container, you would -not- be allowed to delete an augment (using a non-safe-removal solvent) using the tradeskill container.
9. Validated that if the rule is set to a given tradeskill container, you would -not- be allowed to put augments onto armor that would result in an unusable piece of armor based on the slot.
10. Validated that if the rule is set to a given tradeskill container, you would -not- be allowed to allowed to put augments onto armor that you would not be able to wear as your given class / race.
11. Validated that if the rule is set to a given tradeskill container, you would -not- be allowed to cause an item lore exception by putting multiple lore items onto the same armor by accident. Honestly, my guard code here may be superfluous as I was unable to recreate this defect even with the guard code present. Leaving in place and a dev can remove if they believe this is unnecessary and confusing.
12. Verified that only the specified augmentation removal juice would be able to remove a given augment. IE: An augment that requires a XVII solvent could will only be allowed to be removed by an XVII augment.
13. Validated that if the rule is set to a given tradeskill container, you would -not- be allowed to run the augmentation logic (application, removal, and cleanup) if you had more in the container than the specified item and augment.
14. Verified in all of the above cases, that the items in either the tradeskill container or the aug pool world container are deleted if the augmentation succeeded in some fashion (whether deaug'ing or augmenting).
15. Verified that if the rule is set to a given tradeskill container, you would still be able to perform normal tradeskill / recipe combines in it if it did not detect that you were attempting to augment an item. IE: If you combine an item + augment in a spit, you have created a new auged item. If you then combine batwings + frosting in a spit, you have created batwing crunchies.
Overall, I believe I have covered all of the testing cases.
Please let me know if you have any comments or questions.
Please see the below for the patches:
---------------------
Required SQL:
Code:
INSERT INTO RULE_VALUES
SELECT 1, 'Skills:ContainerIdToAllowAugCombines', 0, 'Allows for augmentation combines (normally in a world container augmentation sealer) to also be created in a user held item. This allows for less chance of a lost item on a vulgar zone restart.'
common/ruletypes.h
Code:
Index: ruletypes.h
===================================================================
--- ruletypes.h (revision 1766)
+++ ruletypes.h (working copy)
@@ -92,6 +92,7 @@
RULE_INT ( Skills, MaxTrainTradeskills, 21 )
RULE_BOOL ( Skills, UseLimitTradeskillSearchSkillDiff, true )
RULE_INT ( Skills, MaxTradeskillSearchSkillDiff, 50 )
+RULE_INT ( Skills, ContainerIdToAllowAugCombines, 0 )
RULE_CATEGORY_END()
RULE_CATEGORY( Pets )
common/Item.h
Code:
Index: Item.h
===================================================================
--- Item.h (revision 1766)
+++ Item.h (working copy)
@@ -266,6 +266,7 @@
virtual bool IsStackable() const;
// Can item be equipped by/at?
+ bool SharesACommonSlot(ItemInst* itemToCompare) const;
virtual bool IsEquipable(int16 race, int16 class_) const;
virtual bool IsEquipable(sint16 slot_id) const;
@@ -275,6 +276,8 @@
inline bool IsAugmentable() const { return m_item->AugSlotType[0]!=0; }
bool AvailableWearSlot(uint32 aug_wear_slots) const;
sint8 AvailableAugmentSlot(sint32 augtype) const;
+ inline bool IsAugment() const { return m_item->AugType!=0; }
+ bool WillAugmentCauseLoreCollision(ItemInst* newAugment);
inline sint32 GetAugmentType() const { return m_item->AugType; }
inline bool IsExpendable() const { return ((m_item->Click.Type == ET_Expendable ) || (m_item->ItemType == ItemTypePotion)); }
@@ -283,6 +286,7 @@
// Contents
//
ItemInst* GetItem(uint8 slot) const;
+ uint8 GetAvailableItemCount();
virtual uint32 GetItemID(uint8 slot) const;
inline const ItemInst* operator[](uint8 slot) const { return GetItem(slot); }
void PutItem(uint8 slot, const ItemInst& inst);
common/Item.cpp
Code:
Index: Item.cpp
===================================================================
--- Item.cpp (revision 1766)
+++ Item.cpp (working copy)
@@ -219,6 +219,52 @@
return m_item->IsEquipable(race, class_);
}
+bool ItemInst::WillAugmentCauseLoreCollision(ItemInst* newAugment)
+{
+ bool returnValue = false;
+
+ // If the new augment is an augment -and- it is lore (in some fashion)
+ // perform further checks.
+ if (newAugment->IsAugment() && newAugment->m_item->LoreGroup != 0)
+ {
+ // Check all augments and see if any share the same loregroup status.
+ for(uint8 ctr = 0; ctr < 10; ctr++) {
+
+ ItemInst* itemAugment = this->GetAugment(ctr);
+ if (itemAugment) {
+ // Lore Group Specific check...
+ if (itemAugment->m_item->LoreGroup != -1 && itemAugment->m_item->LoreGroup == newAugment->m_item->LoreGroup) {
+ returnValue = true;
+ break;
+ }
+ // Or they have the same id and they are lore...
+ else if (-1 == newAugment->m_item->LoreGroup && itemAugment->GetID() == newAugment->GetID()) {
+ returnValue = true;
+ break;
+ }
+ }
+ }
+ }
+
+ return returnValue;
+}
+
+bool ItemInst::SharesACommonSlot(ItemInst* itemToCompare) const
+{
+ bool returnValue = false;
+
+ // Go over all possible slots - each slot that is available
+ // in this item, check to see if it is an available slot in the passed
+ // item. If so, exit out.
+ for(int i = 0; i <= 22 && !returnValue; i++) {
+ if (m_item->Slots & ( 1 << i )) {
+ returnValue = itemToCompare->IsEquipable( (i == 22) ? 9999 : i );
+ }
+ }
+
+ return returnValue;
+}
+
// Can equip at this slot?
bool ItemInst::IsEquipable(sint16 slot_id) const
{
@@ -363,6 +409,20 @@
}
}
+uint8 ItemInst::GetAvailableItemCount()
+{
+ uint8 returnValue = 0;
+
+ for(uint8 i = 0; i < 10; i++) {
+ const ItemInst* inst = this->GetItem(i);
+ if (inst) {
+ returnValue++;
+ }
+ }
+
+ return returnValue;
+}
+
// Retrieve item inside container
ItemInst* ItemInst::GetItem(uint8 index) const
{
zone/object.h
Code:
Index: object.h
===================================================================
--- object.h (revision 1766)
+++ object.h (working copy)
@@ -148,7 +148,9 @@
static void HandleCombine(Client* user, const NewCombine_Struct* in_combine, Object *worldo);
static void HandleAugmentation(Client* user, const AugmentItem_Struct* in_augment, Object *worldo);
static void HandleAutoCombine(Client* user, const RecipeAutoCombine_Struct* rac);
-
+ static bool Object::Internal_HandleAugmentation(Client* user, ItemInst* tobe_auged, ItemInst* auged_with, Object *worldo, ItemInst* heldContainer, sint32 augmentationSlotToRemoveFrom, sint32 heldContainer_ContainerSlot);
+ static bool Object::HandleTradeskillAugmentationCombine(Client* user, const NewCombine_Struct* in_combine, ItemInst* container);
+
static SkillType TypeToSkill(uint32 type);
// Packet functions
zone/zonedb.h
Code:
Index: zonedb.h
===================================================================
--- zonedb.h (revision 1766)
+++ zonedb.h (working copy)
@@ -277,6 +277,7 @@
int32 GetZoneForage(int32 ZoneID, int8 skill); /* for foraging - BoB */
int32 GetZoneFishing(int32 ZoneID, int8 skill, uint32 &npc_id, uint8 &npc_chance);
void UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madecount);
+ bool GetIsAllowedAugmentRemoval(uint32 augmentId, uint32 dissolverToTest);
/*
* Tribute
zone/tradeskills.cpp
Code:
Index: tradeskills.cpp
===================================================================
--- tradeskills.cpp (revision 1766)
+++ tradeskills.cpp (working copy)
@@ -53,7 +53,6 @@
}
ItemInst *tobe_auged, *auged_with = NULL;
- sint8 slot=-1;
ItemInst* container = worldo->m_inst;
if (!container) {
@@ -78,40 +77,187 @@
}
}
+ Internal_HandleAugmentation(user,tobe_auged,auged_with,worldo,NULL,in_augment->augment_slot, in_augment->augment_slot);
+}
+
+bool Object::Internal_HandleAugmentation(Client* user, ItemInst* tobe_auged, ItemInst* auged_with, Object *worldo, ItemInst* heldContainer, sint32 augmentationSlotToRemoveFrom, sint32 heldContainer_ContainerSlot)
+{
+ // Flag denoting whether an action was performed for augmentation.
+ // If an aug action was performed successfully, we need to wipe out the users "ingredients"
+ bool wipeContainer = false;
+
// Adding augment
- if (in_augment->augment_slot == -1) {
+ if (augmentationSlotToRemoveFrom == -1) {
+ sint8 slot=-1;
if (((slot=tobe_auged->AvailableAugmentSlot(auged_with->GetAugmentType()))!=-1) && (tobe_auged->AvailableWearSlot(auged_with->GetItem()->Slots))) {
+ wipeContainer = true;
+
tobe_auged->PutAugment(slot,*auged_with);
user->PushItemOnCursor(*tobe_auged,true);
- container->Clear();
+ } else {
+ // Only give warning / error message if this is an official augmentation sealer (world object).
+ if (worldo != NULL) {
+ user->Message(13, "Error: No available slot for augment");
+ }
+ }
+ } else {
+
+ ItemInst *old_aug=NULL;
+ const uint32 id=auged_with->GetID();
+
+ if (id==40408 || id==40409 || id==40410) {
+ tobe_auged->DeleteAugment(augmentationSlotToRemoveFrom);
+ }
+ else {
+ old_aug=tobe_auged->RemoveAugment(augmentationSlotToRemoveFrom);
+ }
+
+ user->PushItemOnCursor(*tobe_auged,true);
+
+ if (old_aug)
+ user->PushItemOnCursor(*old_aug,true);
+
+ wipeContainer = true;
+ }
+
+ if (wipeContainer) {
+ if (worldo != NULL) {
+ // Clear the container
+ worldo->m_inst->Clear();
+
+ // Sent out container clear packer.
EQApplicationPacket* outapp = new EQApplicationPacket(OP_ClearObject, sizeof(ClearObject_Struct));
ClearObject_Struct *cos = (ClearObject_Struct *)outapp->pBuffer;
cos->Clear = 1;
user->QueuePacket(outapp);
safe_delete(outapp);
+
+ // Delete world container contents explicitly.
database.DeleteWorldContainer(worldo->m_id, zone->GetZoneID());
} else {
- user->Message(13, "Error: No available slot for augment");
+ // Delete items in our inventory container...
+ for (uint8 i=0; i<10; i++){
+ const ItemInst* inst = heldContainer->GetItem(i);
+ if (inst) {
+ user->DeleteItemInInventory(Inventory::CalcSlotId(heldContainer_ContainerSlot,i),0,true);
+ }
+ }
+ // Explicitly mark container as cleared.
+ heldContainer->Clear();
}
- } else {
- ItemInst *old_aug=NULL;
- const uint32 id=auged_with->GetID();
- if (id==40408 || id==40409 || id==40410)
- tobe_auged->DeleteAugment(in_augment->augment_slot);
- else
- old_aug=tobe_auged->RemoveAugment(in_augment->augment_slot);
+ }
- user->PushItemOnCursor(*tobe_auged,true);
- if (old_aug)
- user->PushItemOnCursor(*old_aug,true);
- container->Clear();
- EQApplicationPacket* outapp = new EQApplicationPacket(OP_ClearObject, sizeof(ClearObject_Struct));
- ClearObject_Struct *cos = (ClearObject_Struct *)outapp->pBuffer;
- cos->Clear = 1;
+ return wipeContainer;
+}
+
+bool Object::HandleTradeskillAugmentationCombine(Client* user, const NewCombine_Struct* in_combine, ItemInst* container)
+{
+ // Early out - if there are more than two items we are combining, we will not consider an aug combine.
+ if (2 != container->GetAvailableItemCount()){
+ return false;
+ }
+
+ bool augmentationActionHandled = false;
+
+ // For a successful augmentation, we need to have an augmentable item and either an augment or a distiller.
+ ItemInst* toBeAuged = NULL; // If set, this -will- be the item that will be modified.
+ ItemInst* augment = NULL; // If set, this will be the augment we attempt to apply.
+ ItemInst* otherItem = NULL; // If this is set, this -may- be an augment solvent (or similar) that we need to remove.
+
+ // Find the augment and the item to aug - or, the item to aug and the "other item" that may be an augment solvent.
+ int itemsFoundCount = 0;
+ for(uint8 ctr = 0; ctr < 10 && itemsFoundCount < 2; ctr++) {
+
+ ItemInst* itemFound = container->GetItem(ctr);
+ if (itemFound) {
+ itemsFoundCount++;
+ if (itemFound->IsAugmentable()) {
+ toBeAuged = itemFound;
+ } else if (itemFound->IsAugment()) {
+ augment = itemFound;
+ } else {
+ otherItem = itemFound;
+ }
+ }
+ }
+
+ // Now that we know the two items we have to work with, lets see what we can do.
+ // We should have an item to be augmented and either an "other item" (solvent) or an augment.
+ if (toBeAuged) {
+
+ // If we are passed an augment and:
+ // 1. augment is usable by my race/class
+ // 2. shares a common slot with the item to be augmented.
+ // 3. will not cause an augment-lore collision on the item
+ // then let us send the augmentation command to the internal augment function.
+ if (augment) {
+ if (augment->IsEquipable(user->GetPP().race, user->GetPP().class_) &&
+ augment->SharesACommonSlot(toBeAuged) &&
+ !toBeAuged->WillAugmentCauseLoreCollision(augment)
+ ) {
+ // Attempt to augment.
+ augmentationActionHandled = Internal_HandleAugmentation(user,toBeAuged,augment,NULL,container,-1,in_combine->container_slot);
+ }
+ }
+ // If we are not passed an augment and our item-to-augment is not augmented, there is no work to do.
+ else if (toBeAuged->IsAugmented()) {
+ // REMOVE AN AUGMENT.
+
+ // Important thing here - we cannot let users choose which item to remove so let us limit this,
+ // explicitly, to exclude the "delete augment" distillers. Better to make this a safe remove
+ // only rather than to allow functionality that would destroy an augment on accident.
+ //
+
+ const uint32 idOfAssumedSolvent = otherItem->GetID();
+ if(idOfAssumedSolvent==40408 || idOfAssumedSolvent==40409 || idOfAssumedSolvent==40410) {
+ user->Message(13, "Container based augmentation is not allowed for augment deletion. Use a safe-removal solvent or an augmentation world container.");
+
+ // If we are explicitly refusing to handle a case, then there should be no tradeskill combine that is allowed here. Out!
+ augmentationActionHandled = true;
+
+ } else {
+
+ // Time to determine which augment (if any) can be removed by the proposed solvent.
+ // This will hit the database. It -may- be worthwhile to consider pulling this data into the database
+ // all at one time as opposed to performing checks whenever needed. That said, the lookup is a lightweight
+ // one and no performance issues were found when testing. Lets not prematurely optimize this.
+
+ // We will find the first augment that can be removed with this solvent and remove it. If the user wishes
+ // to control de-augmentation in a finer way, the world container is still an option.
+
+ sint32 slotToRemove = -1;
+
+ for(uint8 ctr = 0; ctr < 10; ctr++) {
+
+ ItemInst* augToRemove = toBeAuged->GetAugment(ctr);
+ if (augToRemove) {
+
+ // Check if this item can be removed...
+ if (database.GetIsAllowedAugmentRemoval(augToRemove->GetID(), idOfAssumedSolvent)) {
+ slotToRemove = ctr;
+ break;
+ }
+ }
+ }
+
+ // If we found something to remove, lets do it.
+ if (slotToRemove > -1) {
+ augmentationActionHandled = Internal_HandleAugmentation(user,toBeAuged,otherItem,NULL,container,slotToRemove,in_combine->container_slot);
+ }
+ }
+ }
+ }
+
+ // If we actually -did- something that will stop tradeskills from continuing, let us
+ // go ahead and send the tradeskill combine acknowledgement packet so that the client is not
+ // paused and waiting for us.
+ if (augmentationActionHandled) {
+ EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
user->QueuePacket(outapp);
safe_delete(outapp);
- database.DeleteWorldContainer(worldo->m_id, zone->GetZoneID());
}
+
+ return augmentationActionHandled;
}
// Perform tradeskill combine
@@ -157,6 +303,17 @@
}
container = inst;
+
+ // Do we allow augmentation combines from this container?
+ if (some_id > 0 && RuleI(Skills, ContainerIdToAllowAugCombines)==some_id) {
+
+ // Attempt to handle a possible aug combination.
+ if (HandleTradeskillAugmentationCombine(user, in_combine, container)) {
+
+ // Do no further work as the combine is already handled.
+ return;
+ }
+ }
DBTradeskillRecipe_Struct spec;
if (!database.GetTradeRecipe(container, c_type, some_id, user->CharacterID(), &spec)) {
@@ -1016,7 +1173,36 @@
_log(TRADESKILLS__TRACE, "...Stage2 chance was: %f percent. 0 percent means stage1 failed", chance_stage2);
}
+/// Just a quick check to see if an augment dissolver is allowed to be used against a particular augment.
+bool ZoneDatabase::GetIsAllowedAugmentRemoval(uint32 augmentId, uint32 dissolverToTest)
+{
+ bool returnValue = false;
+ char errbuf[MYSQL_ERRMSG_SIZE];
+ MYSQL_RES *result;
+ char *query = 0;
+ uint32 qlen = 0;
+
+ qlen = MakeAnyLenString(&query, "SELECT 1 FROM items WHERE id = %u AND augdistiller = %u AND id <> 0 AND augdistiller <> 0", augmentId, dissolverToTest);
+
+ if (!RunQuery(query, qlen, errbuf, &result)) {
+ LogFile->write(EQEMuLog::Error, "Error in GetIsAllowedAugmentRemoval search, query: %s", query);
+ safe_delete_array(query);
+ LogFile->write(EQEMuLog::Error, "Error in GetIsAllowedAugmentRemoval search, error: %s", errbuf);
+ return(false);
+ }
+
+ safe_delete_array(query);
+
+ // If even a single row is returned, then augmentation removal is allowed.
+ returnValue = mysql_num_rows(result) > 0;
+
+ // Clean up string allocation.
+ safe_delete_array(query);
+
+ return(returnValue);
+}
+
bool ZoneDatabase::GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id,
uint32 char_id, DBTradeskillRecipe_Struct *spec)
{