Leere
04-16-2010, 06:41 PM
This adds the ability for characters to learn tradeskill recipes through the Experiment mode as well as being taught them via the quest system. Recipes can be flagged as must learn, so that they only show up in searches if the character has been taught them via the quest system. They cannot be learnt via Exerpiment mode if they are flagged as must learn.
The number of how often a recipe has been successfully made is also kept track of. This is meant for things like the variable yield material conversion recipes in Abysmal Sea, where you received more if you had 'practiced' the combine often. (My idea for this would be to off-load that work to the quest system, by creating a new directory for tradeskills and the relevant events. The madecount could then just be a parameter for the event. I didn't try to implement this due to being too unfamiliar with the file loading part of the quest system at this stage.)
Also, searches (and favorite list requests) can be screened for a maximum difference in skill between the trivial and the actual skill. I implemented this because I recalled wanting to skill up on fish rolls when they were quite red. The tradeskill interface would not list them, even though the manual combine attempts via Experiment would not produce a DNC. After the first successful combine a 'You have learned a new recipe...' message (or something with similar wording, so I just picked the closest match in the string DB) was seen and after that the recipe could be found via search recipe.
I don't know if this still applies as such on live, but I remember someone being dismayed about the ease of access to all recipes via the search system on the tradeskill window, so I implemented it with Rules to configure it.
The following two rules handle that last part. I've set the defaults to enable the skill diff limit and the threshold to 50, which is when a recipe starts to be red in the window.
Skills:UseLimitTradeskillSearchSkillDiff
Skills:MaxTradeskillSearchSkillDiff
LearnRecipe(RecipeID) was exported to the quest system for use via a $client object. That should allow the best flexibility for all applications.
I ran the diffs against the files as of Revision 1382.
SQL
create table `char_recipe_list` (
`char_id` int NOT NULL,
`recipe_id` int NOT NULL,
`madecount` int NOT NULL default 0,
primary key (`char_id`, `recipe_id`)
) Engine=InnoDB;
alter table `tradeskill_recipe` add column `must_learn` tinyint not null default 0;
insert into rule_values (ruleset_id, rule_name, rule_value, notes) values
(1, 'Skills:UseLimitTradeskillSearchSkillDiff', 'true', 'Enables the limit for the maximum difference between trivial and skill for recipe searches and favorites.'),
(1, 'Skills:MaxTradeskillSearchSkillDiff', '50', 'The maximum difference in skill between the trivial of an item and the skill of the player if the trivial is higher than the skill. Recipes that have not been learnt or made at least once via the Experiment mode will be removed from searches based on this criteria.');
common\ruletypes.h
--- common\ruletypes.h Thu Mar 25 02:12:12 2010
+++ common\ruletypes.h
@@ -78,6 +78,8 @@
RULE_CATEGORY( Skills )
RULE_INT ( Skills, MaxTrainTradeskills, 21 )
+RULE_BOOL ( Skills, UseLimitTradeskillSearchSkillDiff, true )
+RULE_INT ( Skills, MaxTradeskillSearchSkillDiff, 50 )
RULE_CATEGORY_END()
RULE_CATEGORY( Pets )
zone\tradeskills.cpp
--- zone\tradeskills.cpp Mon Nov 30 03:57:32 2009
+++ zone\tradeskills.cpp
@@ -149,7 +149,7 @@
container = inst;
DBTradeskillRecipe_Struct spec;
- if (!database.GetTradeRecipe(container, c_type, some_id, &spec)) {
+ if (!database.GetTradeRecipe(container, c_type, some_id, user->CharacterID(), &spec)) {
user->Message_StringID(4,TRADESKILL_NOCOMBINE);
EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
user->QueuePacket(outapp);
@@ -157,6 +157,16 @@
return;
}
+ // Character hasn't learnt the recipe yet.
+ if (spec.must_learn && !spec.has_learnt) {
+ // Made up message for the client. Just giving a DNC is the other option.
+ user->Message(4, "You need to learn how to combine these first.");
+ EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
+ user->QueuePacket(outapp);
+ safe_delete(outapp);
+ return;
+ }
+
//changing from a switch to string of if's since we don't need to iterate through all of the skills in the SkillType enum
if (spec.tradeskill == ALCHEMY) {
if (user_pp.class_ != SHAMAN) {
@@ -207,6 +217,16 @@
//do the check and send results...
bool success = user->TradeskillExecute(&spec);
+ // Learn new recipe message
+ // Update Made count
+ // TODO: Trigger EVENT for scripts
+ if (success) {
+ if (!spec.has_learnt) {
+ user->Message_StringID(4, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
+ }
+ database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
+ }
+
// Replace the container on success if required.
//
@@ -234,12 +254,23 @@
//ask the database for the recipe to make sure it exists...
DBTradeskillRecipe_Struct spec;
- if (!database.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, &spec)) {
+ if (!database.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, user->CharacterID(), &spec)) {
LogFile->write(EQEMuLog::Error, "Unknown recipe for HandleAutoCombine: %u\n", rac->recipe_id);
user->QueuePacket(outapp);
safe_delete(outapp);
return;
}
+
+ // Character hasn't learnt the recipe yet.
+ // This shouldn't happen.
+ if (spec.must_learn && !spec.has_learnt) {
+ // Made up message for the client. Just giving a DNC is the other option.
+ user->Message(4, "You need to learn how to combine these first.");
+ user->QueuePacket(outapp);
+ safe_delete(outapp);
+ return;
+ }
+
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -349,6 +380,14 @@
bool success = user->TradeskillExecute(&spec);
+ if (success) {
+ if (!spec.has_learnt) {
+ user->Message_StringID(4, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
+ }
+ database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
+ }
+
+
//TODO: find in-pack containers in inventory, make sure they are really
//there, and then use that slot to handle replace_container too.
if(success && spec.replace_container) {
@@ -441,7 +480,7 @@
//search gave no results... not an error
return;
}
- if(mysql_num_fields(result) != 4) {
+ if(mysql_num_fields(result) != 6) {
LogFile->write(EQEMuLog::Error, "Error in TradeskillSearchResults query '%s': Invalid column count in result", query);
return;
}
@@ -449,13 +488,25 @@
uint8 r;
for(r = 0; r < qcount; r++) {
row = mysql_fetch_row(result);
- if(row == NULL || row[0] == NULL || row[1] == NULL || row[2] == NULL || row[3] == NULL)
+ if(row == NULL || row[0] == NULL || row[1] == NULL || row[2] == NULL || row[3] == NULL || row[5] == NULL)
continue;
uint32 recipe = (uint32)atoi(row[0]);
const char *name = row[1];
uint32 trivial = (uint32) atoi(row[2]);
uint32 comp_count = (uint32) atoi(row[3]);
+ uint32 tradeskill = (uint16) atoi(row[5]);
+ // Skip the recipes that exceed the threshold in skill difference
+ // Recipes that have either been made before or were
+ // explicitly learned are excempt from that limit
+ if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff)) {
+ if ((trivial - GetSkill((SkillType)tradeskill)) > RuleI(Skills, MaxTradeskillSearchSkillDiff)
+ && row[4] == NULL
+ )
+ continue;
+ }
+
+
EQApplicationPacket* outapp = new EQApplicationPacket(OP_RecipeReply, sizeof(RecipeReply_Struct));
RecipeReply_Struct *reply = (RecipeReply_Struct *) outapp->pBuffer;
@@ -913,7 +964,7 @@
bool ZoneDatabase::GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id,
- DBTradeskillRecipe_Struct *spec)
+ uint32 char_id, DBTradeskillRecipe_Struct *spec)
{
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -1105,12 +1156,12 @@
return false;
}
- return(GetTradeRecipe(recipe_id, c_type, some_id, spec));
+ return(GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec));
}
bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id,
- DBTradeskillRecipe_Struct *spec)
+ uint32 char_id, DBTradeskillRecipe_Struct *spec)
{
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -1131,11 +1182,13 @@
}
qlen = MakeAnyLenString(&query, "SELECT tr.id, tr.tradeskill, tr.skillneeded,"
- " tr.trivial, tr.nofail, tr.replace_container, tr.name"
+ " tr.trivial, tr.nofail, tr.replace_container, tr.name, tr.must_learn, crl.madecount"
" FROM tradeskill_recipe AS tr inner join tradeskill_recipe_entries as tre"
" ON tr.id = tre.recipe_id"
+ " LEFT JOIN (SELECT recipe_id, madecount from char_recipe_list WHERE char_id = %u) AS crl "
+ " ON tr.id = crl.recipe_id "
" WHERE tr.id = %lu AND tre.item_id %s"
- " GROUP BY tr.id", (unsigned long)recipe_id, containers);
+ " GROUP BY tr.id", char_id, (unsigned long)recipe_id, containers);
if (!RunQuery(query, qlen, errbuf, &result)) {
LogFile->write(EQEMuLog::Error, "Error in GetTradeRecipe, query: %s", query);
@@ -1158,6 +1211,15 @@
spec->nofail = atoi(row[4]) ? true : false;
spec->replace_container = atoi(row[5]) ? true : false;
spec->name = row[6];
+ spec->must_learn = atoi(row[7]) ? true : false;
+ if (row[8] == NULL) {
+ spec->has_learnt = false;
+ spec->madecount = 0;
+ } else {
+ spec->has_learnt = true;
+ spec->madecount = (uint32)atoul(row[8]);
+ }
+ spec->recipe_id = recipe_id;
mysql_free_result(result);
//Pull the on-success items...
@@ -1222,4 +1284,73 @@
return(true);
}
+void ZoneDatabase::UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madecount)
+{
+ char *query = 0;
+ uint32 qlen;
+ char errbuf[MYSQL_ERRMSG_SIZE];
+
+ qlen = MakeAnyLenString(&query, "INSERT INTO char_recipe_list "
+ " SET recipe_id = %u, char_id = %u, madecount = %u "
+ " ON DUPLICATE KEY UPDATE madecount = %u;"
+ , recipe_id, char_id, madecount, madecount);
+ if (!RunQuery(query, qlen, errbuf)) {
+ LogFile->write(EQEMuLog::Error, "Error in UpdateRecipeMadecount query '%s': %s", query, errbuf);
+ }
+ safe_delete_array(query);
+}
+
+
+void Client::LearnRecipe(uint32 recipeID)
+{
+ char *query = 0;
+ uint32 qlen;
+ uint32 qcount = 0;
+ char errbuf[MYSQL_ERRMSG_SIZE];
+ MYSQL_RES *result;
+ MYSQL_ROW row;
+
+ qlen = MakeAnyLenString(&query, "SELECT tr.name, crl.madecount "
+ " FROM tradeskill_recipe as tr "
+ " LEFT JOIN (SELECT recipe_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl "
+ " ON tr.id = crl.recipe_id "
+ " WHERE tr.id = %u ;", CharacterID(), recipeID);
+
+ if (!database.RunQuery(query, qlen, errbuf, &result)) {
+ LogFile->write(EQEMuLog::Error, "Error in Client::LearnRecipe query '%s': %s", query, errbuf);
+ safe_delete_array(query);
+ return;
+ }
+
+ qcount = mysql_num_rows(result);
+ if (qcount != 1) {
+ LogFile->write(EQEMuLog::Normal, "Client::LearnRecipe - RecipeID: %d had %d occurences.", recipeID, qcount);
+ mysql_free_result(result);
+ safe_delete_array(query);
+ return;
+ }
+ safe_delete_array(query);
+
+ row = mysql_fetch_row(result);
+
+ if (row != NULL && row[0] != NULL) {
+ // Only give Learn message if character doesn't know the recipe
+ if (row[1] == NULL) {
+ Message_StringID(4, TRADESKILL_LEARN_RECIPE, row[0]);
+ // Actually learn the recipe now
+ qlen = MakeAnyLenString(&query, "INSERT INTO char_recipe_list "
+ " SET recipe_id = %u, char_id = %u, madecount = 0 "
+ " ON DUPLICATE KEY UPDATE madecount = madecount;"
+ , recipeID, CharacterID());
+
+ if (!database.RunQuery(query, qlen, errbuf)) {
+ LogFile->write(EQEMuLog::Error, "Error in LearnRecipe query '%s': %s", query, errbuf);
+ }
+ safe_delete_array(query);
+ }
+ }
+
+ mysql_free_result(result);
+
+}
zone\zonedb.h
--- zone\zonedb.h Wed Oct 28 12:42:57 2009
+++ zone\zonedb.h
@@ -43,6 +43,10 @@
vector< pair<uint32,uint8> > onsuccess;
vector< pair<uint32,uint8> > onfail;
string name;
+ bool must_learn;
+ bool has_learnt;
+ uint32 madecount;
+ uint32 recipe_id;
};
struct PetRecord {
@@ -269,10 +273,11 @@
/*
* Tradeskills
*/
- bool GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id, DBTradeskillRecipe_Struct *spec);
- bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, DBTradeskillRecipe_Struct *spec);
+ bool GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec);
+ bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec);
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);
/*
* Tribute
zone\StringIDs.h
--- zone\StringIDs.h Fri Apr 9 13:55:48 2010
+++ zone\StringIDs.h
@@ -191,6 +191,7 @@
#define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.'
#define ONLY_SUMMONED_PETS 3269 //3269 This effect only works with summoned pets.
#define SUSPEND_MINION_FIGHTING 3270 //Your pet must be at peace, first.
+#define TRADESKILL_LEARN_RECIPE 3457 //You have learned the recipe %1!
#define WHOALL_NO_RESULTS 5029 //There are no players in EverQuest that match those who filters.
#define PETITION_NO_DELETE 5053 //You do not have a petition in the queue.
#define PETITION_DELETED 5054 //Your petition was successfully deleted.
Add to public section of class Client in zone\client.h
void LearnRecipe(uint32 recipeID);
zone\perl_client.cpp
--- zone\perl_client.cpp Thu Mar 25 02:12:16 2010
+++ zone\perl_client.cpp
@@ -4336,6 +4336,32 @@
XSRETURN(1);
}
+XS(XS_Client_LearnRecipe);
+XS(XS_Client_LearnRecipe)
+{
+ dXSARGS;
+ if (items != 2)
+ Perl_croak(aTHX_ "Usage: Client::LearnRecipe(THIS, recipe_id)");
+ {
+ Client * THIS;
+ dXSTARG;
+ uint32 recipe_id = (uint32)SvUV(ST(1));
+
+ if (sv_derived_from(ST(0), "Client")) {
+ IV tmp = SvIV((SV*)SvRV(ST(0)));
+ THIS = INT2PTR(Client *,tmp);
+ }
+ else
+ Perl_croak(aTHX_ "THIS is not of type Client");
+ if(THIS == NULL)
+ Perl_croak(aTHX_ "THIS is NULL, avoiding crash.");
+
+ THIS->LearnRecipe(recipe_id);;
+ }
+ XSRETURN_EMPTY;
+}
+
+
#ifdef __cplusplus
extern "C"
#endif
@@ -4516,6 +4542,7 @@
newXSproto(strcpy(buf, "UpdateGroupAAs"), XS_Client_UpdateGroupAAs, file, "$$$");
newXSproto(strcpy(buf, "GetGroupPoints"), XS_Client_GetGroupPoints, file, "$");
newXSproto(strcpy(buf, "GetRaidPoints"), XS_Client_GetRaidPoints, file, "$");
+ newXSproto(strcpy(buf, "LearnRecipe"), XS_Client_LearnRecipe, file, "$$");
XSRETURN_YES;
}
zone\client_packet.cpp
--- zone\client_packet.cpp Fri Apr 16 19:11:36 2010
+++ zone\client_packet.cpp
@@ -5290,13 +5290,15 @@
//To be a good kid, I should move this SQL somewhere else...
//but im lazy right now, so it stays here
uint32 qlen = 0;
- qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount) "
+ qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount),c rl.madecount,tr.tradeskill "
" FROM tradeskill_recipe AS tr "
" LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id=tre.recipe_id "
+ " LEFT JOIN (SELECT recipe_id, char_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id "
" WHERE tr.id IN (%s) "
+ " AND ((tr.must_learn <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
- " LIMIT 100 ", buf, containers);
+ " LIMIT 100 ", CharacterID(), buf, containers);
TradeskillSearchResults(query, qlen, tsf->object_type, tsf->some_id);
@@ -5343,14 +5343,16 @@
uint32 qlen = 0;
//arbitrary limit of 200 recipes, makes sense to me.
- qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount) "
+ qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount),c rl.madecount,tr.tradeskill "
" FROM tradeskill_recipe AS tr "
" LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id=tre.recipe_id "
+ " LEFT JOIN (SELECT recipe_id, char_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id "
" WHERE %s tr.trivial >= %u AND tr.trivial <= %u "
+ " AND ((tr.must_learn <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
" LIMIT 200 "
- , searchclause, rss->mintrivial, rss->maxtrivial, containers);
+ , CharacterID(), searchclause, rss->mintrivial, rss->maxtrivial, containers);
TradeskillSearchResults(query, qlen, rss->object_type, rss->some_id);
The number of how often a recipe has been successfully made is also kept track of. This is meant for things like the variable yield material conversion recipes in Abysmal Sea, where you received more if you had 'practiced' the combine often. (My idea for this would be to off-load that work to the quest system, by creating a new directory for tradeskills and the relevant events. The madecount could then just be a parameter for the event. I didn't try to implement this due to being too unfamiliar with the file loading part of the quest system at this stage.)
Also, searches (and favorite list requests) can be screened for a maximum difference in skill between the trivial and the actual skill. I implemented this because I recalled wanting to skill up on fish rolls when they were quite red. The tradeskill interface would not list them, even though the manual combine attempts via Experiment would not produce a DNC. After the first successful combine a 'You have learned a new recipe...' message (or something with similar wording, so I just picked the closest match in the string DB) was seen and after that the recipe could be found via search recipe.
I don't know if this still applies as such on live, but I remember someone being dismayed about the ease of access to all recipes via the search system on the tradeskill window, so I implemented it with Rules to configure it.
The following two rules handle that last part. I've set the defaults to enable the skill diff limit and the threshold to 50, which is when a recipe starts to be red in the window.
Skills:UseLimitTradeskillSearchSkillDiff
Skills:MaxTradeskillSearchSkillDiff
LearnRecipe(RecipeID) was exported to the quest system for use via a $client object. That should allow the best flexibility for all applications.
I ran the diffs against the files as of Revision 1382.
SQL
create table `char_recipe_list` (
`char_id` int NOT NULL,
`recipe_id` int NOT NULL,
`madecount` int NOT NULL default 0,
primary key (`char_id`, `recipe_id`)
) Engine=InnoDB;
alter table `tradeskill_recipe` add column `must_learn` tinyint not null default 0;
insert into rule_values (ruleset_id, rule_name, rule_value, notes) values
(1, 'Skills:UseLimitTradeskillSearchSkillDiff', 'true', 'Enables the limit for the maximum difference between trivial and skill for recipe searches and favorites.'),
(1, 'Skills:MaxTradeskillSearchSkillDiff', '50', 'The maximum difference in skill between the trivial of an item and the skill of the player if the trivial is higher than the skill. Recipes that have not been learnt or made at least once via the Experiment mode will be removed from searches based on this criteria.');
common\ruletypes.h
--- common\ruletypes.h Thu Mar 25 02:12:12 2010
+++ common\ruletypes.h
@@ -78,6 +78,8 @@
RULE_CATEGORY( Skills )
RULE_INT ( Skills, MaxTrainTradeskills, 21 )
+RULE_BOOL ( Skills, UseLimitTradeskillSearchSkillDiff, true )
+RULE_INT ( Skills, MaxTradeskillSearchSkillDiff, 50 )
RULE_CATEGORY_END()
RULE_CATEGORY( Pets )
zone\tradeskills.cpp
--- zone\tradeskills.cpp Mon Nov 30 03:57:32 2009
+++ zone\tradeskills.cpp
@@ -149,7 +149,7 @@
container = inst;
DBTradeskillRecipe_Struct spec;
- if (!database.GetTradeRecipe(container, c_type, some_id, &spec)) {
+ if (!database.GetTradeRecipe(container, c_type, some_id, user->CharacterID(), &spec)) {
user->Message_StringID(4,TRADESKILL_NOCOMBINE);
EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
user->QueuePacket(outapp);
@@ -157,6 +157,16 @@
return;
}
+ // Character hasn't learnt the recipe yet.
+ if (spec.must_learn && !spec.has_learnt) {
+ // Made up message for the client. Just giving a DNC is the other option.
+ user->Message(4, "You need to learn how to combine these first.");
+ EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
+ user->QueuePacket(outapp);
+ safe_delete(outapp);
+ return;
+ }
+
//changing from a switch to string of if's since we don't need to iterate through all of the skills in the SkillType enum
if (spec.tradeskill == ALCHEMY) {
if (user_pp.class_ != SHAMAN) {
@@ -207,6 +217,16 @@
//do the check and send results...
bool success = user->TradeskillExecute(&spec);
+ // Learn new recipe message
+ // Update Made count
+ // TODO: Trigger EVENT for scripts
+ if (success) {
+ if (!spec.has_learnt) {
+ user->Message_StringID(4, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
+ }
+ database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
+ }
+
// Replace the container on success if required.
//
@@ -234,12 +254,23 @@
//ask the database for the recipe to make sure it exists...
DBTradeskillRecipe_Struct spec;
- if (!database.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, &spec)) {
+ if (!database.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, user->CharacterID(), &spec)) {
LogFile->write(EQEMuLog::Error, "Unknown recipe for HandleAutoCombine: %u\n", rac->recipe_id);
user->QueuePacket(outapp);
safe_delete(outapp);
return;
}
+
+ // Character hasn't learnt the recipe yet.
+ // This shouldn't happen.
+ if (spec.must_learn && !spec.has_learnt) {
+ // Made up message for the client. Just giving a DNC is the other option.
+ user->Message(4, "You need to learn how to combine these first.");
+ user->QueuePacket(outapp);
+ safe_delete(outapp);
+ return;
+ }
+
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -349,6 +380,14 @@
bool success = user->TradeskillExecute(&spec);
+ if (success) {
+ if (!spec.has_learnt) {
+ user->Message_StringID(4, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
+ }
+ database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
+ }
+
+
//TODO: find in-pack containers in inventory, make sure they are really
//there, and then use that slot to handle replace_container too.
if(success && spec.replace_container) {
@@ -441,7 +480,7 @@
//search gave no results... not an error
return;
}
- if(mysql_num_fields(result) != 4) {
+ if(mysql_num_fields(result) != 6) {
LogFile->write(EQEMuLog::Error, "Error in TradeskillSearchResults query '%s': Invalid column count in result", query);
return;
}
@@ -449,13 +488,25 @@
uint8 r;
for(r = 0; r < qcount; r++) {
row = mysql_fetch_row(result);
- if(row == NULL || row[0] == NULL || row[1] == NULL || row[2] == NULL || row[3] == NULL)
+ if(row == NULL || row[0] == NULL || row[1] == NULL || row[2] == NULL || row[3] == NULL || row[5] == NULL)
continue;
uint32 recipe = (uint32)atoi(row[0]);
const char *name = row[1];
uint32 trivial = (uint32) atoi(row[2]);
uint32 comp_count = (uint32) atoi(row[3]);
+ uint32 tradeskill = (uint16) atoi(row[5]);
+ // Skip the recipes that exceed the threshold in skill difference
+ // Recipes that have either been made before or were
+ // explicitly learned are excempt from that limit
+ if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff)) {
+ if ((trivial - GetSkill((SkillType)tradeskill)) > RuleI(Skills, MaxTradeskillSearchSkillDiff)
+ && row[4] == NULL
+ )
+ continue;
+ }
+
+
EQApplicationPacket* outapp = new EQApplicationPacket(OP_RecipeReply, sizeof(RecipeReply_Struct));
RecipeReply_Struct *reply = (RecipeReply_Struct *) outapp->pBuffer;
@@ -913,7 +964,7 @@
bool ZoneDatabase::GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id,
- DBTradeskillRecipe_Struct *spec)
+ uint32 char_id, DBTradeskillRecipe_Struct *spec)
{
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -1105,12 +1156,12 @@
return false;
}
- return(GetTradeRecipe(recipe_id, c_type, some_id, spec));
+ return(GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec));
}
bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id,
- DBTradeskillRecipe_Struct *spec)
+ uint32 char_id, DBTradeskillRecipe_Struct *spec)
{
char errbuf[MYSQL_ERRMSG_SIZE];
MYSQL_RES *result;
@@ -1131,11 +1182,13 @@
}
qlen = MakeAnyLenString(&query, "SELECT tr.id, tr.tradeskill, tr.skillneeded,"
- " tr.trivial, tr.nofail, tr.replace_container, tr.name"
+ " tr.trivial, tr.nofail, tr.replace_container, tr.name, tr.must_learn, crl.madecount"
" FROM tradeskill_recipe AS tr inner join tradeskill_recipe_entries as tre"
" ON tr.id = tre.recipe_id"
+ " LEFT JOIN (SELECT recipe_id, madecount from char_recipe_list WHERE char_id = %u) AS crl "
+ " ON tr.id = crl.recipe_id "
" WHERE tr.id = %lu AND tre.item_id %s"
- " GROUP BY tr.id", (unsigned long)recipe_id, containers);
+ " GROUP BY tr.id", char_id, (unsigned long)recipe_id, containers);
if (!RunQuery(query, qlen, errbuf, &result)) {
LogFile->write(EQEMuLog::Error, "Error in GetTradeRecipe, query: %s", query);
@@ -1158,6 +1211,15 @@
spec->nofail = atoi(row[4]) ? true : false;
spec->replace_container = atoi(row[5]) ? true : false;
spec->name = row[6];
+ spec->must_learn = atoi(row[7]) ? true : false;
+ if (row[8] == NULL) {
+ spec->has_learnt = false;
+ spec->madecount = 0;
+ } else {
+ spec->has_learnt = true;
+ spec->madecount = (uint32)atoul(row[8]);
+ }
+ spec->recipe_id = recipe_id;
mysql_free_result(result);
//Pull the on-success items...
@@ -1222,4 +1284,73 @@
return(true);
}
+void ZoneDatabase::UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madecount)
+{
+ char *query = 0;
+ uint32 qlen;
+ char errbuf[MYSQL_ERRMSG_SIZE];
+
+ qlen = MakeAnyLenString(&query, "INSERT INTO char_recipe_list "
+ " SET recipe_id = %u, char_id = %u, madecount = %u "
+ " ON DUPLICATE KEY UPDATE madecount = %u;"
+ , recipe_id, char_id, madecount, madecount);
+ if (!RunQuery(query, qlen, errbuf)) {
+ LogFile->write(EQEMuLog::Error, "Error in UpdateRecipeMadecount query '%s': %s", query, errbuf);
+ }
+ safe_delete_array(query);
+}
+
+
+void Client::LearnRecipe(uint32 recipeID)
+{
+ char *query = 0;
+ uint32 qlen;
+ uint32 qcount = 0;
+ char errbuf[MYSQL_ERRMSG_SIZE];
+ MYSQL_RES *result;
+ MYSQL_ROW row;
+
+ qlen = MakeAnyLenString(&query, "SELECT tr.name, crl.madecount "
+ " FROM tradeskill_recipe as tr "
+ " LEFT JOIN (SELECT recipe_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl "
+ " ON tr.id = crl.recipe_id "
+ " WHERE tr.id = %u ;", CharacterID(), recipeID);
+
+ if (!database.RunQuery(query, qlen, errbuf, &result)) {
+ LogFile->write(EQEMuLog::Error, "Error in Client::LearnRecipe query '%s': %s", query, errbuf);
+ safe_delete_array(query);
+ return;
+ }
+
+ qcount = mysql_num_rows(result);
+ if (qcount != 1) {
+ LogFile->write(EQEMuLog::Normal, "Client::LearnRecipe - RecipeID: %d had %d occurences.", recipeID, qcount);
+ mysql_free_result(result);
+ safe_delete_array(query);
+ return;
+ }
+ safe_delete_array(query);
+
+ row = mysql_fetch_row(result);
+
+ if (row != NULL && row[0] != NULL) {
+ // Only give Learn message if character doesn't know the recipe
+ if (row[1] == NULL) {
+ Message_StringID(4, TRADESKILL_LEARN_RECIPE, row[0]);
+ // Actually learn the recipe now
+ qlen = MakeAnyLenString(&query, "INSERT INTO char_recipe_list "
+ " SET recipe_id = %u, char_id = %u, madecount = 0 "
+ " ON DUPLICATE KEY UPDATE madecount = madecount;"
+ , recipeID, CharacterID());
+
+ if (!database.RunQuery(query, qlen, errbuf)) {
+ LogFile->write(EQEMuLog::Error, "Error in LearnRecipe query '%s': %s", query, errbuf);
+ }
+ safe_delete_array(query);
+ }
+ }
+
+ mysql_free_result(result);
+
+}
zone\zonedb.h
--- zone\zonedb.h Wed Oct 28 12:42:57 2009
+++ zone\zonedb.h
@@ -43,6 +43,10 @@
vector< pair<uint32,uint8> > onsuccess;
vector< pair<uint32,uint8> > onfail;
string name;
+ bool must_learn;
+ bool has_learnt;
+ uint32 madecount;
+ uint32 recipe_id;
};
struct PetRecord {
@@ -269,10 +273,11 @@
/*
* Tradeskills
*/
- bool GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id, DBTradeskillRecipe_Struct *spec);
- bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, DBTradeskillRecipe_Struct *spec);
+ bool GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec);
+ bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec);
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);
/*
* Tribute
zone\StringIDs.h
--- zone\StringIDs.h Fri Apr 9 13:55:48 2010
+++ zone\StringIDs.h
@@ -191,6 +191,7 @@
#define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.'
#define ONLY_SUMMONED_PETS 3269 //3269 This effect only works with summoned pets.
#define SUSPEND_MINION_FIGHTING 3270 //Your pet must be at peace, first.
+#define TRADESKILL_LEARN_RECIPE 3457 //You have learned the recipe %1!
#define WHOALL_NO_RESULTS 5029 //There are no players in EverQuest that match those who filters.
#define PETITION_NO_DELETE 5053 //You do not have a petition in the queue.
#define PETITION_DELETED 5054 //Your petition was successfully deleted.
Add to public section of class Client in zone\client.h
void LearnRecipe(uint32 recipeID);
zone\perl_client.cpp
--- zone\perl_client.cpp Thu Mar 25 02:12:16 2010
+++ zone\perl_client.cpp
@@ -4336,6 +4336,32 @@
XSRETURN(1);
}
+XS(XS_Client_LearnRecipe);
+XS(XS_Client_LearnRecipe)
+{
+ dXSARGS;
+ if (items != 2)
+ Perl_croak(aTHX_ "Usage: Client::LearnRecipe(THIS, recipe_id)");
+ {
+ Client * THIS;
+ dXSTARG;
+ uint32 recipe_id = (uint32)SvUV(ST(1));
+
+ if (sv_derived_from(ST(0), "Client")) {
+ IV tmp = SvIV((SV*)SvRV(ST(0)));
+ THIS = INT2PTR(Client *,tmp);
+ }
+ else
+ Perl_croak(aTHX_ "THIS is not of type Client");
+ if(THIS == NULL)
+ Perl_croak(aTHX_ "THIS is NULL, avoiding crash.");
+
+ THIS->LearnRecipe(recipe_id);;
+ }
+ XSRETURN_EMPTY;
+}
+
+
#ifdef __cplusplus
extern "C"
#endif
@@ -4516,6 +4542,7 @@
newXSproto(strcpy(buf, "UpdateGroupAAs"), XS_Client_UpdateGroupAAs, file, "$$$");
newXSproto(strcpy(buf, "GetGroupPoints"), XS_Client_GetGroupPoints, file, "$");
newXSproto(strcpy(buf, "GetRaidPoints"), XS_Client_GetRaidPoints, file, "$");
+ newXSproto(strcpy(buf, "LearnRecipe"), XS_Client_LearnRecipe, file, "$$");
XSRETURN_YES;
}
zone\client_packet.cpp
--- zone\client_packet.cpp Fri Apr 16 19:11:36 2010
+++ zone\client_packet.cpp
@@ -5290,13 +5290,15 @@
//To be a good kid, I should move this SQL somewhere else...
//but im lazy right now, so it stays here
uint32 qlen = 0;
- qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount) "
+ qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount),c rl.madecount,tr.tradeskill "
" FROM tradeskill_recipe AS tr "
" LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id=tre.recipe_id "
+ " LEFT JOIN (SELECT recipe_id, char_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id "
" WHERE tr.id IN (%s) "
+ " AND ((tr.must_learn <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
- " LIMIT 100 ", buf, containers);
+ " LIMIT 100 ", CharacterID(), buf, containers);
TradeskillSearchResults(query, qlen, tsf->object_type, tsf->some_id);
@@ -5343,14 +5343,16 @@
uint32 qlen = 0;
//arbitrary limit of 200 recipes, makes sense to me.
- qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount) "
+ qlen = MakeAnyLenString(&query, "SELECT tr.id,tr.name,tr.trivial,SUM(tre.componentcount),c rl.madecount,tr.tradeskill "
" FROM tradeskill_recipe AS tr "
" LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id=tre.recipe_id "
+ " LEFT JOIN (SELECT recipe_id, char_id, madecount FROM char_recipe_list WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id "
" WHERE %s tr.trivial >= %u AND tr.trivial <= %u "
+ " AND ((tr.must_learn <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
" LIMIT 200 "
- , searchclause, rss->mintrivial, rss->maxtrivial, containers);
+ , CharacterID(), searchclause, rss->mintrivial, rss->maxtrivial, containers);
TradeskillSearchResults(query, qlen, rss->object_type, rss->some_id);