소스 검색

Itemlink command and integration (#7291)

Co-authored-by: Akkarinage <mike.langford@industrial-illusions.net>
Co-authored-by: Aleos <aleos89@users.noreply.github.com>
Co-authored-by: Atemo <Atemo@users.noreply.github.com>
Co-authored-by: cydh<cydh@users.noreply.github.com>
Co-authored-by: Lemongrass3110 <lemongrass@kstp.at>
Co-authored-by: secretdataz<secretdataz@users.noreply.github.com>
SapitoSucio 2 년 전
부모
커밋
55d3c1578c
13개의 변경된 파일287개의 추가작업 그리고 17개의 파일을 삭제
  1. 7 0
      conf/battle/feature.conf
  2. 23 0
      doc/script_commands.txt
  3. 15 0
      npc/test/ci/0000_funcs.txt
  4. 35 0
      npc/test/ci/7291.txt
  5. 30 0
      src/common/utilities.cpp
  6. 25 0
      src/common/utilities.hpp
  7. 15 15
      src/map/atcommand.cpp
  8. 1 0
      src/map/battle.cpp
  9. 1 0
      src/map/battle.hpp
  10. 94 0
      src/map/itemdb.cpp
  11. 3 0
      src/map/itemdb.hpp
  12. 36 0
      src/map/script.cpp
  13. 2 2
      tools/ci/npc.sh

+ 7 - 0
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 <ITEML> 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

+ 23 - 0
doc/script_commands.txt

@@ -10958,6 +10958,29 @@ If <char id> is specified, the specified player is used rather than the attached
 
 ---------------------------------------
 
+*itemlink(<item_id>,<refine>,<card0>,<card1>,<card2>,<card3>,<enchantgrade>{,<RandomIDArray>,<RandomValueArray>,<RandomParamArray>});
+
+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.|
 ========================

+ 15 - 0
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;
+}

+ 35 - 0
npc/test/ci/7291.txt

@@ -0,0 +1,35 @@
+-	script	itemlink#ci	-1,{
+OnInit:
+	if( checkre(0) ){
+		if( PACKETVER >= 20200916 ){
+			.@expected$ = "<ITEML>0000213v0%0g&00'00)18X)1ck)00)00+2R,00-00</ITEML>";
+		}else if( PACKETVER >= 20150225 ){
+			// Grade does not exist (clientside) yet
+			.@expected$ = "<ITEMLINK>0000213v0%0g&00(18X(1ck(00(00*2R+00,00</ITEMLINK>";
+		}else if( PACKETVER >= 20100000 ){
+			// Random Options do not exist (clientside) yet
+			.@expected$ = "<ITEMLINK>0000213v0%0g&00(18X(1ck(00(00</ITEMLINK>";
+		}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$ = "<ITEML>000021hS%0a&00'00)18X)00)00)00</ITEML>";
+		}else if( PACKETVER >= 20100000 ){
+			// Grade does not exist (clientside) yet// Grade does not exist (clientside) yet
+			.@expected$ = "<ITEMLINK>000021hS%0a&00(18X(00(00(00</ITEMLINK>";
+		}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]");
+	}
+}

+ 30 - 0
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;
+}

+ 25 - 0
src/common/utilities.hpp

@@ -285,6 +285,31 @@ namespace rathena {
 		template <typename T> 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 );
 	}
 }
 

+ 15 - 15
src/map/atcommand.cpp

@@ -4007,7 +4007,7 @@ ACMD_FUNC(idsearch)
 	for(const auto &result : item_array) {
 		std::shared_ptr<item_data> 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> 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<item_data> 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 )

+ 1 - 0
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,        },

+ 1 - 0
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;

+ 94 - 0
src/map/itemdb.cpp

@@ -5,6 +5,8 @@
 
 #include <iostream>
 #include <stdlib.h>
+#include <math.h>
+#include <unordered_map>
 
 #include "../common/nullpo.hpp"
 #include "../common/random.hpp"
@@ -1239,6 +1241,98 @@ std::shared_ptr<item_data> ItemDatabase::searchname( const char *name ){
 	return util::umap_find( this->nameToItemDataMap, lowername );
 }
 
+/**
+* Generates an item link string
+* @param data: Item info
+* @return <ITEML> string for the item
+* @author [Cydh]
+**/
+std::string ItemDatabase::create_item_link( struct item& item ){
+	std::shared_ptr<item_data> 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 = "<ITEML>";
+	const std::string closing_tag = "</ITEML>";
+#else // PACKETVER >= 20100000
+	const std::string start_tag = "<ITEMLINK>";
+	const std::string closing_tag = "</ITEMLINK>";
+#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;
 
 /**

+ 3 - 0
src/map/itemdb.hpp

@@ -5,6 +5,7 @@
 #define ITEMDB_HPP
 
 #include <map>
+#include <string>
 #include <vector>
 
 #include "../common/database.hpp"
@@ -1344,6 +1345,8 @@ public:
 	// Additional
 	std::shared_ptr<item_data> searchname( const char* name );
 	std::shared_ptr<item_data> 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;

+ 36 - 0
src/map/script.cpp

@@ -26774,6 +26774,41 @@ BUILDIN_FUNC(item_enchant){
 #endif
 }
 
+/**
+* Generate item link string for client
+* itemlink(<item_id>,<refine>,<card0>,<card1>,<card2>,<card3>,<enchantgrade>{,<RandomIDArray>,<RandomValueArray>,<RandomParamArray>});
+* @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, "?"),

+ 2 - 2
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