From 55d3c1578cc6e9cc925d17f23fbacc8a0bafd8d0 Mon Sep 17 00:00:00 2001 From: SapitoSucio Date: Fri, 11 Nov 2022 11:04:41 -0600 Subject: [PATCH] Itemlink command and integration (#7291) Co-authored-by: Akkarinage Co-authored-by: Aleos Co-authored-by: Atemo Co-authored-by: cydh Co-authored-by: Lemongrass3110 Co-authored-by: secretdataz --- conf/battle/feature.conf | 7 +++ doc/script_commands.txt | 23 ++++++++++ npc/test/ci/0000_funcs.txt | 15 ++++++ npc/test/ci/7291.txt | 35 ++++++++++++++ src/common/utilities.cpp | 30 ++++++++++++ src/common/utilities.hpp | 25 ++++++++++ src/map/atcommand.cpp | 30 ++++++------ src/map/battle.cpp | 1 + src/map/battle.hpp | 1 + src/map/itemdb.cpp | 94 ++++++++++++++++++++++++++++++++++++++ src/map/itemdb.hpp | 3 ++ src/map/script.cpp | 36 +++++++++++++++ tools/ci/npc.sh | 4 +- 13 files changed, 287 insertions(+), 17 deletions(-) create mode 100644 npc/test/ci/0000_funcs.txt create mode 100644 npc/test/ci/7291.txt diff --git a/conf/battle/feature.conf b/conf/battle/feature.conf index aef58b5519..64f7f4fa2e 100644 --- a/conf/battle/feature.conf +++ b/conf/battle/feature.conf @@ -135,3 +135,10 @@ feature.dynamicnpc_rangey: 2 // Should the dynamic NPCs look into the direction of the player? (Note 1) // Default: no feature.dynamicnpc_direction: no + +// Itemlink System on informational related commands (Note 1) +// Generates string for an item and can be used for npctalk, message, +// dispbottom, and broadcast commands. The result is clickable-item name just +// like from SHIFT+Click from player's inventory/cart/equipment window. +// Requires: 2010-00-00RagexeRE or later +feature.itemlink: on diff --git a/doc/script_commands.txt b/doc/script_commands.txt index a763813cf0..7ca22f201d 100644 --- a/doc/script_commands.txt +++ b/doc/script_commands.txt @@ -10958,6 +10958,29 @@ If is specified, the specified player is used rather than the attached --------------------------------------- +*itemlink(,,,,,,{,,,}); + +Generates an item link string for an item that can be used for npctalk, message, +dispbottom, and broadcast commands. The result is a clickable-item name just +like SHIFT+Click from a player's inventory/cart/equipment window. This command can be +used with mes but the item name will not be clickable. You should use the normal client +tags for displaying item links in mes dialogues, if the client supports them. + + +Examples: + + npctalk "Knife [3] : "+itemlink(1201)+""; + npctalk "+16 Knife [3] : "+itemlink(1201,16)+""; + npctalk "+13 BXB Bapho+VR+EA2+EA1 : "+itemlink(18110,13,4147,4407,4833,4832)+""; + setarray .@opt_ids[0],RDMOPT_VAR_ATKPERCENT,RDMOPT_VAR_ATKPERCENT,RDMOPT_VAR_ATTMPOWER,0,0; + setarray .@opt_values[0],3,5,20,0,0; + setarray .@opt_params[0],0,0,0,0,0; + npctalk "+13 BXB Bapho+VR+EA2+EA1 + 3 Options : "+itemlink(18110,13,4147,4407,4833,4832,0,.@opt_ids,.@opt_values,.@opt_params)+""; + + +RandomIDArray, RandomValueArray, and RandomParamArray only works if the +client (and server) supports the Item Random Options feature (PACKETVER >= 20150225). + ======================== |14.- Channel commands.| ======================== diff --git a/npc/test/ci/0000_funcs.txt b/npc/test/ci/0000_funcs.txt new file mode 100644 index 0000000000..c874f504e6 --- /dev/null +++ b/npc/test/ci/0000_funcs.txt @@ -0,0 +1,15 @@ +function script AssertTrue { + if (!getarg(0)) { + errormes "AssertTrue failed for " + getarg(1) + "."; + return false; + } + return true; +} + +function script AssertEquals { + if (getarg(0) != getarg(1)) { + errormes "AssertEquals failed for " + getarg(2) + ": expected " + getarg(0) + ", got " + getarg(1) + "."; + return false; + } + return true; +} \ No newline at end of file diff --git a/npc/test/ci/7291.txt b/npc/test/ci/7291.txt new file mode 100644 index 0000000000..9d960f71d2 --- /dev/null +++ b/npc/test/ci/7291.txt @@ -0,0 +1,35 @@ +- script itemlink#ci -1,{ +OnInit: + if( checkre(0) ){ + if( PACKETVER >= 20200916 ){ + .@expected$ = "0000213v0%0g&00'00)18X)1ck)00)00+2R,00-00"; + }else if( PACKETVER >= 20150225 ){ + // Grade does not exist (clientside) yet + .@expected$ = "0000213v0%0g&00(18X(1ck(00(00*2R+00,00"; + }else if( PACKETVER >= 20100000 ){ + // Random Options do not exist (clientside) yet + .@expected$ = "0000213v0%0g&00(18X(1ck(00(00"; + }else{ + // Item Link does not exist (clientside) yet + .@expected$ = "Crimson Saber"; + } + + setarray .@opt_ids,RDMOPT_WEAPON_ATTR_GROUND; + .@actual$ = itemlink(13454,16,4399,4608,0,0,0,.@opt_ids,.@opt_dummy,.@opt_dummy); + AssertEquals(.@expected$, .@actual$, "Generated itemlink for +16 Earth Crimson Saber"); + }else{ + if( PACKETVER >= 20200916 ){ + .@expected$ = "000021hS%0a&00'00)18X)00)00)00"; + }else if( PACKETVER >= 20100000 ){ + // Grade does not exist (clientside) yet// Grade does not exist (clientside) yet + .@expected$ = "000021hS%0a&00(18X(00(00(00"; + }else{ + // Item Link does not exist (clientside) yet + .@expected$ = "Blade"; + } + + // No Random Options in Pre-Renewal + .@actual$ = itemlink(1108,10,4399); + AssertEquals(.@expected$, .@actual$, "Generated itemlink for +10 Blade[4]"); + } +} diff --git a/src/common/utilities.cpp b/src/common/utilities.cpp index 6e5e50a9cd..48adb85e3f 100644 --- a/src/common/utilities.cpp +++ b/src/common/utilities.cpp @@ -114,3 +114,33 @@ bool rathena::util::safe_multiplication( int64 a, int64 b, int64& result ){ return false; #endif } + +void rathena::util::string_left_pad_inplace(std::string& str, char padding, size_t num) +{ + str.insert(0, min(0, num - str.length()), padding); +} + +std::string rathena::util::string_left_pad(const std::string& original, char padding, size_t num) +{ + return std::string(num - min(num, original.length()), padding) + original; +} + +constexpr char base62_dictionary[] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z' +}; + +std::string rathena::util::base62_encode( uint32 val ){ + std::string result = ""; + while (val != 0) { + result = base62_dictionary[(val % 62)] + result; + val /= 62; + } + return result; +} diff --git a/src/common/utilities.hpp b/src/common/utilities.hpp index a7109f5792..cbd6de8702 100644 --- a/src/common/utilities.hpp +++ b/src/common/utilities.hpp @@ -285,6 +285,31 @@ namespace rathena { template void tolower( T& string ){ std::transform( string.begin(), string.end(), string.begin(), ::tolower ); } + + /** + * Pad string with arbitrary character in-place + * @param str: String to pad + * @param padding: Padding character + * @param num: Maximum length of padding + */ + void string_left_pad_inplace(std::string& str, char padding, size_t num); + + /** + * Pad string with arbitrary character + * @param original: String to pad + * @param padding: Padding character + * @param num: Maximum length of padding + * + * @return A copy of original string with padding added + */ + std::string string_left_pad(const std::string& original, char padding, size_t num); + + /** + * Encode base10 number to base62. Originally by lututui + * @param val: Base10 Number + * @return Base62 string + **/ + std::string base62_encode( uint32 val ); } } diff --git a/src/map/atcommand.cpp b/src/map/atcommand.cpp index 79e299d4e7..fb9a1357b0 100644 --- a/src/map/atcommand.cpp +++ b/src/map/atcommand.cpp @@ -4007,7 +4007,7 @@ ACMD_FUNC(idsearch) for(const auto &result : item_array) { std::shared_ptr id = result.second; - sprintf(atcmd_output, msg_txt(sd,78), id->ename.c_str(), id->nameid); // %s: %u + sprintf(atcmd_output, msg_txt(sd,78), item_db.create_item_link( id->nameid ).c_str(), id->nameid); // %s: %u clif_displaymessage(fd, atcmd_output); } sprintf(atcmd_output, msg_txt(sd,79), match); // It is %d affair above. @@ -6678,7 +6678,7 @@ ACMD_FUNC(autolootitem) return -1; } sd->state.autolootid[i] = item_data->nameid; // Autoloot Activated - sprintf(atcmd_output, msg_txt(sd,1192), item_data->name.c_str(), item_data->ename.c_str(), item_data->nameid); // Autolooting item: '%s'/'%s' {%u} + sprintf(atcmd_output, msg_txt(sd,1192), item_data->name.c_str(), item_db.create_item_link( item_data->nameid ).c_str(), item_data->nameid); // Autolooting item: '%s'/'%s' {%u} clif_displaymessage(fd, atcmd_output); sd->state.autolooting = 1; break; @@ -6689,7 +6689,7 @@ ACMD_FUNC(autolootitem) return -1; } sd->state.autolootid[i] = 0; - sprintf(atcmd_output, msg_txt(sd,1194), item_data->name.c_str(), item_data->ename.c_str(), item_data->nameid); // Removed item: '%s'/'%s' {%u} from your autolootitem list. + sprintf(atcmd_output, msg_txt(sd,1194), item_data->name.c_str(), item_db.create_item_link( item_data->nameid ).c_str(), item_data->nameid); // Removed item: '%s'/'%s' {%u} from your autolootitem list. clif_displaymessage(fd, atcmd_output); ARR_FIND(0, AUTOLOOTITEM_SIZE, i, sd->state.autolootid[i] != 0); if (i == AUTOLOOTITEM_SIZE) { @@ -6717,7 +6717,7 @@ ACMD_FUNC(autolootitem) continue; } - sprintf(atcmd_output, "'%s'/'%s' {%u}", item_data->name.c_str(), item_data->ename.c_str(), item_data->nameid); + sprintf(atcmd_output, "'%s'/'%s' {%u}", item_data->name.c_str(), item_db.create_item_link( item_data->nameid ).c_str(), item_data->nameid); clif_displaymessage(fd, atcmd_output); } } @@ -7784,9 +7784,9 @@ ACMD_FUNC(mobinfo) int droprate = mob_getdroprate( &sd->bl, mob, mob->dropitem[i].rate, drop_modifier ); if (id->slots) - sprintf(atcmd_output2, " - %s[%d] %02.02f%%", id->ename.c_str(), id->slots, (float)droprate / 100); + sprintf(atcmd_output2, " - %s[%d] %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), id->slots, (float)droprate / 100); else - sprintf(atcmd_output2, " - %s %02.02f%%", id->ename.c_str(), (float)droprate / 100); + sprintf(atcmd_output2, " - %s %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), (float)droprate / 100); strcat(atcmd_output, atcmd_output2); if (++j % 3 == 0) { clif_displaymessage(fd, atcmd_output); @@ -7824,14 +7824,14 @@ ACMD_FUNC(mobinfo) j++; if (j == 1) { if (id->slots) - sprintf(atcmd_output2, " %s[%d] %02.02f%%", id->ename.c_str(), id->slots, mvppercent); + sprintf(atcmd_output2, " %s[%d] %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), id->slots, mvppercent); else - sprintf(atcmd_output2, " %s %02.02f%%", id->ename.c_str(), mvppercent); + sprintf(atcmd_output2, " %s %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), mvppercent); } else { if (id->slots) - sprintf(atcmd_output2, " - %s[%d] %02.02f%%", id->ename.c_str(), id->slots, mvppercent); + sprintf(atcmd_output2, " - %s[%d] %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), id->slots, mvppercent); else - sprintf(atcmd_output2, " - %s %02.02f%%", id->ename.c_str(), mvppercent); + sprintf(atcmd_output2, " - %s %02.02f%%", item_db.create_item_link( id->nameid ).c_str(), mvppercent); } strcat(atcmd_output, atcmd_output2); } @@ -8266,7 +8266,7 @@ ACMD_FUNC(iteminfo) std::shared_ptr item_data = result.second; sprintf(atcmd_output, msg_txt(sd,1277), // Item: '%s'/'%s'[%d] (%u) Type: %s | Extra Effect: %s - item_data->name.c_str(),item_data->ename.c_str(),item_data->slots,item_data->nameid, + item_data->name.c_str(), item_db.create_item_link( item_data->nameid ).c_str(),item_data->slots,item_data->nameid, (item_data->type != IT_AMMO) ? itemdb_typename((enum item_types)item_data->type) : itemdb_typename_ammo((e_ammo_type)item_data->subtype), (item_data->script==NULL)? msg_txt(sd,1278) : msg_txt(sd,1279) // None / With script ); @@ -8323,7 +8323,7 @@ ACMD_FUNC(whodrops) for (const auto &result : item_array) { std::shared_ptr id = result.second; - sprintf(atcmd_output, msg_txt(sd,1285), id->ename.c_str(), id->slots, id->nameid); // Item: '%s'[%d] (ID:%u) + sprintf(atcmd_output, msg_txt(sd,1285), item_db.create_item_link( id->nameid ).c_str(), id->slots, id->nameid); // Item: '%s'[%d] (ID:%u) clif_displaymessage(fd, atcmd_output); if (id->mob[0].chance == 0) { @@ -9319,9 +9319,9 @@ ACMD_FUNC(itemlist) } if( it->refine ) - StringBuf_Printf(&buf, "%d %s %+d (%s, id: %u)", it->amount, itd->ename.c_str(), it->refine, itd->name.c_str(), it->nameid); + StringBuf_Printf(&buf, "%d %s %+d (%s, id: %u)", it->amount, item_db.create_item_link( it->nameid ).c_str(), it->refine, itd->name.c_str(), it->nameid); else - StringBuf_Printf(&buf, "%d %s (%s, id: %u)", it->amount, itd->ename.c_str(), itd->name.c_str(), it->nameid); + StringBuf_Printf(&buf, "%d %s (%s, id: %u)", it->amount, item_db.create_item_link( it->nameid ).c_str(), itd->name.c_str(), it->nameid); if( it->equip ) { char equipstr[CHAT_SIZE_MAX]; @@ -9424,7 +9424,7 @@ ACMD_FUNC(itemlist) if( counter2 != 1 ) StringBuf_AppendStr(&buf, ", "); - StringBuf_Printf(&buf, "#%d %s (id: %u)", counter2, card->ename.c_str(), card->nameid); + StringBuf_Printf(&buf, "#%d %s (id: %u)", counter2, item_db.create_item_link( card->nameid ).c_str(), card->nameid); } if( counter2 > 0 ) diff --git a/src/map/battle.cpp b/src/map/battle.cpp index 84528f82cb..041a981ef4 100644 --- a/src/map/battle.cpp +++ b/src/map/battle.cpp @@ -10267,6 +10267,7 @@ static const struct _battle_data { { "feature.barter", &battle_config.feature_barter, 1, 0, 1, }, { "feature.barter_extended", &battle_config.feature_barter_extended, 1, 0, 1, }, + { "feature.itemlink", &battle_config.feature_itemlink, 1, 0, 1, }, { "break_mob_equip", &battle_config.break_mob_equip, 0, 0, 1, }, { "macro_detection_retry", &battle_config.macro_detection_retry, 3, 1, INT_MAX, }, { "macro_detection_timeout", &battle_config.macro_detection_timeout, 60000, 0, INT_MAX, }, diff --git a/src/map/battle.hpp b/src/map/battle.hpp index 15bd21a493..d416c622be 100644 --- a/src/map/battle.hpp +++ b/src/map/battle.hpp @@ -563,6 +563,7 @@ struct Battle_Config int discount_item_point_shop; int update_enemy_position; int devotion_rdamage; + int feature_itemlink; // autotrade persistency int feature_autotrade; diff --git a/src/map/itemdb.cpp b/src/map/itemdb.cpp index 0c18e8aca7..439f015196 100644 --- a/src/map/itemdb.cpp +++ b/src/map/itemdb.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include "../common/nullpo.hpp" #include "../common/random.hpp" @@ -1239,6 +1241,98 @@ std::shared_ptr ItemDatabase::searchname( const char *name ){ return util::umap_find( this->nameToItemDataMap, lowername ); } +/** +* Generates an item link string +* @param data: Item info +* @return string for the item +* @author [Cydh] +**/ +std::string ItemDatabase::create_item_link( struct item& item ){ + std::shared_ptr data = this->find( item.nameid ); + + if( data == nullptr ){ + ShowError( "Tried to create itemlink for unknown item %u.\n", item.nameid ); + return "Unknown item"; + } + +// All these dates are unconfirmed +#if PACKETVER >= 20100000 + if( !battle_config.feature_itemlink ){ + // Feature is disabled + return data->ename; + } + + struct item_data* id = data.get(); + +#if PACKETVER_MAIN_NUM >= 20200916 || PACKETVER_RE_NUM >= 20200724 + const std::string start_tag = ""; + const std::string closing_tag = ""; +#else // PACKETVER >= 20100000 + const std::string start_tag = ""; + const std::string closing_tag = ""; +#endif + + std::string itemstr = start_tag; + + itemstr += util::string_left_pad(util::base62_encode(id->equip), '0', 5); + itemstr += itemdb_isequip2(id) ? "1" : "0"; + itemstr += util::base62_encode(item.nameid); + if (item.refine > 0) { + itemstr += "%" + util::string_left_pad(util::base62_encode(item.refine), '0', 2); + } + if (itemdb_isequip2(id)) { + itemstr += "&" + util::string_left_pad(util::base62_encode(id->look), '0', 2); + } +#if PACKETVER_MAIN_NUM >= 20200916 || PACKETVER_RE_NUM >= 20200724 + itemstr += "'" + util::string_left_pad(util::base62_encode(item.enchantgrade), '0', 2); +#endif + +#if PACKETVER_MAIN_NUM >= 20200916 || PACKETVER_RE_NUM >= 20200724 + const std::string card_sep = ")"; + const std::string optid_sep = "+"; + const std::string optpar_sep = ","; + const std::string optval_sep = "-"; +#else + const std::string card_sep = "("; + const std::string optid_sep = "*"; + const std::string optpar_sep = "+"; + const std::string optval_sep = ","; +#endif + + for (uint8 i = 0; i < MAX_SLOTS; ++i) { + itemstr += card_sep + util::string_left_pad(util::base62_encode(item.card[i]), '0', 2); + } + +#if PACKETVER >= 20150225 + for (uint8 i = 0; i < MAX_ITEM_RDM_OPT; ++i) { + if (item.option[i].id == 0) { + break; // ignore options including ones beyond this one since the client won't even display them + } + // Option ID + itemstr += optid_sep + util::string_left_pad(util::base62_encode(item.option[i].id), '0', 2); + // Param + itemstr += optpar_sep + util::string_left_pad(util::base62_encode(item.option[i].param), '0', 2); + // Value + itemstr += optval_sep + util::string_left_pad(util::base62_encode(item.option[i].value), '0', 2); + } +#endif + + itemstr += closing_tag; + return itemstr; +#else + // Did not exist before that + return data->ename; +#endif +} + +std::string ItemDatabase::create_item_link( t_itemid id ){ + struct item it = {}; + + it.nameid = id; + + return this->create_item_link( it ); +} + ItemDatabase item_db; /** diff --git a/src/map/itemdb.hpp b/src/map/itemdb.hpp index 724877bb3c..ce5a08cf81 100644 --- a/src/map/itemdb.hpp +++ b/src/map/itemdb.hpp @@ -5,6 +5,7 @@ #define ITEMDB_HPP #include +#include #include #include "../common/database.hpp" @@ -1344,6 +1345,8 @@ public: // Additional std::shared_ptr searchname( const char* name ); std::shared_ptr search_aegisname( const char *name ); + std::string create_item_link( struct item& data ); + std::string create_item_link( t_itemid id ); }; extern ItemDatabase item_db; diff --git a/src/map/script.cpp b/src/map/script.cpp index 633f21f6a9..3df4089fd4 100644 --- a/src/map/script.cpp +++ b/src/map/script.cpp @@ -26774,6 +26774,41 @@ BUILDIN_FUNC(item_enchant){ #endif } +/** +* Generate item link string for client +* itemlink(,,,,,,{,,,}); +* @author [Cydh] +**/ +BUILDIN_FUNC(itemlink) +{ + struct item item = {}; + + item.nameid = script_getnum(st, 2); + + if( !item_db.exists( item.nameid ) ){ + ShowError( "buildin_itemlink: Item ID %u does not exists.\n", item.nameid ); + st->state = END; + return SCRIPT_CMD_FAILURE; + } + + FETCH(3, item.refine); + FETCH(4, item.card[0]); + FETCH(5, item.card[1]); + FETCH(6, item.card[2]); + FETCH(7, item.card[3]); + FETCH(8, item.enchantgrade); + +#if PACKETVER >= 20150225 + if ( script_hasdata(st,9) && script_getitem_randomoption(st, nullptr, &item, "itemlink", 9) == false) { + st->state = END; + return SCRIPT_CMD_FAILURE; + } +#endif + + std::string itemlstr = item_db.create_item_link(item); + script_pushstrcopy(st, itemlstr.c_str()); + return SCRIPT_CMD_SUCCESS; +} BUILDIN_FUNC(addfame) { struct map_session_data *sd; @@ -27558,6 +27593,7 @@ struct script_function buildin_func[] = { BUILDIN_DEF(add_reputation_points, "ii?"), BUILDIN_DEF(item_reform, "??"), BUILDIN_DEF(item_enchant, "i?"), + BUILDIN_DEF(itemlink, "i?????????"), BUILDIN_DEF(addfame, "i?"), BUILDIN_DEF(getfame, "?"), BUILDIN_DEF(getfamerank, "?"), diff --git a/tools/ci/npc.sh b/tools/ci/npc.sh index 98fb6f4f88..739aec979e 100755 --- a/tools/ci/npc.sh +++ b/tools/ci/npc.sh @@ -5,8 +5,8 @@ out=npc/scripts_custom.conf printf "\n" >> $out echo "// Custom Scripts" >> $out -find npc/custom \( -name "*.txt" \) | xargs -I % echo "npc: %" >> $out +find npc/custom \( -name "*.txt" \) | sort | xargs -I % echo "npc: %" >> $out echo "// Test Scripts" >> $out -find npc/test \( -name "*.txt" \) | xargs -I % echo "npc: %" >> $out +find npc/test \( -name "*.txt" \) | sort | xargs -I % echo "npc: %" >> $out