PDA

View Full Version : COMMITTED: Learning Recipes


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);

joligario
04-16-2010, 09:55 PM
Nice. I have 2 questions before you implement this:

They cannot be learnt via Exerpiment mode if they are flagged as must learn.

What about the recipes that are required to learn through experiment mode, but only after 1 successful combine? The Abysmal element for this did change the recipe learning dynamics some, but the older world recipes were still a 1-time learning experience.

Since you are adding functionality to recipe filtering, should we add a flag for unlearnable? There are certain recipes that were unlearnable no matter what.

Leere
04-17-2010, 05:20 AM
I was under the impression that the Abysmal Sea quests would work with NPCs needing to teach the recipes. (EQTC link (http://eqtraders.com/articles/article_page.php?article=q190&menustr=120037000000)) Mind you, that link is pretty much the source for all of my knowledge of those specific quests, so it's not as if I have any first hand knowledge.

Either way, the refinements you suggested.

I decided to just treat the must_learn field like a bitfield:
bit 1 (0x1): Must learn via quest system
bit 2 (0x2): Can learn via experiment mode, only will show up after being learnt, no matter the trivial.
bit 5 (0x10): Don't show learn message, use unlisted to remove from search
bit 6 (0x20): Unlisted recipe

Bit 1 will override Bit 2. 5 and 6 don't conflict with anything. The learn message suppression only applies for any generated by combines. LearnRecipe will is not affected.

I think that should cover all combinations that are of interest. Please let me know if you can think of any others.

I'm only repeating the files that had any modifications from the first post. zone\client_packet is now a bit dated, sorry about that, the changes there are limited to the queries in Handle_OP_RecipesFavorite and Handle_OP_RecipesSearch, if the line numbers are completely off now.

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,21 @@
return;
}

+ // Character hasn't learnt the recipe yet.
+ // must_learn:
+ // bit 1 (0x01): recipe can't be experimented
+ // bit 2 (0x02): can try to experiment but not useable for auto-combine until learnt
+ // bit 5 (0x10): no learn message, use unlisted flag to prevent it showing up on search
+ // bit 6 (0x20): unlisted recipe flag
+ if ((spec.must_learn&0xF) == 1 && !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 +222,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 && ((spec.must_learn&0x10) != 0x10)) {
+ 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 +259,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&0xf) && !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 +385,14 @@

bool success = user->TradeskillExecute(&spec);

+ if (success) {
+ if (!spec.has_learnt && ((spec.must_learn & 0x10) != 0x10)) {
+ 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 +485,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 +493,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]);

+ // Leere: 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 +969,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 +1161,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 +1187,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 +1216,15 @@
spec->nofail = atoi(row[4]) ? true : false;
spec->replace_container = atoi(row[5]) ? true : false;
spec->name = row[6];
+ spec->must_learn = (uint8)atoi(row[7]);
+ 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 +1289,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\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, 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 & 0x20 = 0 AND((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 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, 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 & 0x20 = 0 AND ((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 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);



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;
+ uint8 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

cavedude
04-17-2010, 09:55 PM
Great work! This is committed in revision 1392.

cavedude
04-18-2010, 10:12 AM
Looks like this has one bug: http://www.projecteq.net/phpBB2/viewtopic.php?t=10111 I don't believe basic tradeskills ever need to be learned, and at this point all recipes have a 0 for must_learn.

Leere
04-18-2010, 12:56 PM
Bit logic is evil when you're tired... *embarrassed* And Java breeds very stupid habits for datatypes in expressions... *headdesk*

--- zone\client_packet.cpp Sun Apr 18 04:44:01 2010
+++ zone\client_packet.cpp
@@ -5295,7 +5295,7 @@
" LEFT 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 IN (%s) "
- " AND tr.must_learn & 0x20 = 0 AND((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 0)) "
+ " AND tr.must_learn & 0x20 <> 0x20 AND ((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
" LIMIT 100 ", CharacterID(), buf, containers);
@@ -5350,7 +5350,7 @@
" LEFT 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 %s tr.trivial >= %u AND tr.trivial <= %u "
- " AND tr.must_learn & 0x20 = 0 AND ((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 0)) "
+ " AND tr.must_learn & 0x20 <> 0x20 AND((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) OR (tr.must_learn & 0x3 = 0)) "
" GROUP BY tr.id "
" HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 "
" LIMIT 200 "


--- zone\tradeskills.cpp Sun Apr 18 04:44:01 2010
+++ zone\tradeskills.cpp
@@ -505,10 +505,11 @@
// 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
- )
+ if (((sint32)trivial - (sint32)GetSkill((SkillType)tradeskill)) > RuleI(Skills, MaxTradeskillSearchSkillDiff)
+ && row[4] == NULL)
+ {
continue;
+ }
}

cavedude
04-18-2010, 05:19 PM
Bit logic is evil.

Corrected that ^ for you. Thanks, I'll get this committed now!

joligario
02-24-2011, 10:00 PM
Leere,

Trying to wrap my head around your bit logic for the messages because something still doesn't seem right. Here is what I am thinking:

0x00 - Not learned. No message.
0x01 - Learned via quest. If it is quest learned, your learn recipe sub handles the learned message.
0x02 - Learned via experiment. There should be a message about learning the recipe upon the first successful combine.
0x10 - Learned without message. No message.
0x20 - Unlisted. No message.

However, as it is now, you get a message in all instances except 0x10.

Leere
02-26-2011, 06:33 AM
Sorry about the late reply, I haven't been visiting the site daily lately.

The learn_recipe bit field was basically envisioned to work in the following way. The lower half-byte is used to decide how the recipe is available (0 - only skill limits, 1 - quest system has to make it available, 2 - user experiments or quest system makes available), while the upper half-byte holds actual flags you can add freely to the lower setting. (0x10 - suppress the learn message, 0x20 - don't return the recipe for favorites and general recipe searches).

So, as an example, you could set must_learn to 0x31, which should mean that there is no learn message given, the recipe can't be searched for, and it has to be made available via the quest system.

That means there's a learn message for 0x00 for cases where a manual combine is made for a red recipe, basically. I didn't limit it to just those though since I was under the impression that the message was also there any time you made a combine for the first time. I could well be mistaken about that though.

I've since come to understand that currently this also gives learn messages for quest combines. I've been meaning to compile a list to update those with the 0x10 flag, but something else has always kept coming up.

I've also forgot to check for the suppression flag in LearnRecipe, now that I've looked at the code again. Or I was thinking you'd only call that if you actually wanted the user to see a message anyway. Either way, it's inconsistent.


Since I need to fix the learn message flag for LearnRecipe anyway I can change or expand on that behavior, if you want. Just please let me know what you'd prefer.

joligario
02-26-2011, 02:03 PM
No problem.

Actually, I think your LearnRecipe() function is correct. It should post the message to the user. I think you are fine without changing that. As I read the rest of the logic, I think I see what you were doing. I need to do more testing. It is possible that our editor is not setting the bits right. I'll let you know what I find after I finish my homework and stuff.

Thanks for the info!