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>
This commit is contained in:
SapitoSucio 2022-11-11 11:04:41 -06:00 committed by GitHub
parent 96efb0e426
commit 55d3c1578c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 287 additions and 17 deletions

View File

@ -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

View File

@ -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.|
========================

View File

@ -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
npc/test/ci/7291.txt Normal file
View File

@ -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]");
}
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -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, },

View File

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

View File

@ -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;
/**

View File

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

View File

@ -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, "?"),

View File

@ -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