// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL // Copyright (c) Hercules Dev Team - Licensed under GNU GPL // For more information, see LICENCE in the main folder #include "clif.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "achievement.hpp" #include "atcommand.hpp" #include "battle.hpp" #include "battleground.hpp" #include "cashshop.hpp" #include "channel.hpp" #include "chat.hpp" #include "chrif.hpp" #include "clan.hpp" #include "clif.hpp" #include "elemental.hpp" #include "guild.hpp" #include "homunculus.hpp" #include "instance.hpp" #include "intif.hpp" #include "itemdb.hpp" #include "log.hpp" #include "mail.hpp" #include "map.hpp" #include "mercenary.hpp" #include "mob.hpp" #include "npc.hpp" #include "party.hpp" #include "pc.hpp" #include "pc_groups.hpp" #include "pet.hpp" #include "quest.hpp" #include "script.hpp" #include "skill.hpp" #include "status.hpp" #include "storage.hpp" #include "trade.hpp" #include "unit.hpp" #include "vending.hpp" using namespace rathena; static inline uint32 client_tick( t_tick tick ){ return (uint32)tick; } #if PACKETVER >= 20170830 static inline int64 client_exp(t_exp exp) { return (int64)u64min(exp, MAX_EXP); } #else static inline int32 client_exp(t_exp exp) { return (int32)u64min(exp, MAX_EXP); } #endif /* for clif_clearunit_delayed */ static struct eri *delay_clearunit_ers; struct s_packet_db packet_db[MAX_PACKET_DB + 1]; unsigned long color_table[COLOR_MAX]; #include "clif_obfuscation.hpp" static bool clif_session_isValid(map_session_data *sd); static void clif_loadConfirm( map_session_data *sd ); static void clif_favorite_item( map_session_data& sd, uint16 index ); #if PACKETVER >= 20150513 enum mail_type { MAIL_TYPE_TEXT = 0x0, MAIL_TYPE_ZENY = 0x2, MAIL_TYPE_ITEM = 0x4, MAIL_TYPE_NPC = 0x8 }; #endif enum e_inventory_type{ INVTYPE_INVENTORY = 0, INVTYPE_CART = 1, INVTYPE_STORAGE = 2, INVTYPE_GUILD_STORAGE = 3, }; /** Converts item type to display it on client if necessary. * @param nameid: Item ID * @return item type. For IT_PETEGG will be displayed as IT_ARMOR. If Shadow Weapon of IT_SHADOWGEAR as IT_WEAPON and else as IT_ARMOR */ static inline int itemtype(t_itemid nameid) { struct item_data* id = itemdb_search(nameid); //Use itemdb_search, so non-existance item will use dummy data and won't crash the server. bugreport:8468 int type = id->type; if( type == IT_SHADOWGEAR ) { if( id->equip&EQP_SHADOW_WEAPON ) return IT_WEAPON; else return IT_ARMOR; } return ( type == IT_PETEGG ) ? IT_ARMOR : type; } // TODO: doc static inline uint16 client_index( uint16 server_index ){ return server_index + 2; } static inline uint16 server_index( uint16 client_index ){ return client_index - 2; } static inline uint16 client_storage_index( uint16 server_index ){ return server_index + 1; } static inline uint16 server_storage_index( uint16 client_index ){ return client_index - 1; } static inline uint32 disguised_bl_id( uint32 bl_id ){ // Casting to prevent a compiler warning return -((int32)bl_id); } #if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 static inline uint32 client_nameid( t_itemid server_nameid ){ t_itemid view = itemdb_viewid( server_nameid ); if( view > 0 ){ return view; }else{ return server_nameid; } } #else static inline uint16 client_nameid( t_itemid server_nameid ){ t_itemid view = itemdb_viewid( server_nameid ); if( view > 0 ){ if( view > UINT16_MAX ){ return (uint16)UNKNOWN_ITEM_ID; } return (uint16)view; }else{ if( server_nameid > UINT16_MAX ){ return (uint16)UNKNOWN_ITEM_ID; } return (uint16)server_nameid; } } #endif static inline void WBUFPOS(uint8* p, unsigned short pos, short x, short y, unsigned char dir) { p += pos; p[0] = (uint8)(x>>2); p[1] = (uint8)((x<<6) | ((y>>4)&0x3f)); p[2] = (uint8)((y<<4) | (dir&0xf)); } // client-side: x0+=sx0*0.0625-0.5 and y0+=sy0*0.0625-0.5 static inline void WBUFPOS2(uint8* p, unsigned short pos, short x0, short y0, short x1, short y1, unsigned char sx0, unsigned char sy0) { p += pos; p[0] = (uint8)(x0>>2); p[1] = (uint8)((x0<<6) | ((y0>>4)&0x3f)); p[2] = (uint8)((y0<<4) | ((x1>>6)&0x0f)); p[3] = (uint8)((x1<<2) | ((y1>>8)&0x03)); p[4] = (uint8)y1; p[5] = (uint8)((sx0<<4) | (sy0&0x0f)); } static inline void WFIFOPOS(int fd, unsigned short pos, short x, short y, unsigned char dir) { WBUFPOS(WFIFOP(fd,pos), 0, x, y, dir); } static inline void RBUFPOS(const uint8* p, unsigned short pos, short* x, short* y, unsigned char* dir) { p += pos; if( x ) { x[0] = ( ( p[0] & 0xff ) << 2 ) | ( p[1] >> 6 ); } if( y ) { y[0] = ( ( p[1] & 0x3f ) << 4 ) | ( p[2] >> 4 ); } if( dir ) { dir[0] = ( p[2] & 0x0f ); } } static inline void RBUFPOS2(const uint8* p, unsigned short pos, short* x0, short* y0, short* x1, short* y1, unsigned char* sx0, unsigned char* sy0) { p += pos; if( x0 ) { x0[0] = ( ( p[0] & 0xff ) << 2 ) | ( p[1] >> 6 ); } if( y0 ) { y0[0] = ( ( p[1] & 0x3f ) << 4 ) | ( p[2] >> 4 ); } if( x1 ) { x1[0] = ( ( p[2] & 0x0f ) << 6 ) | ( p[3] >> 2 ); } if( y1 ) { y1[0] = ( ( p[3] & 0x03 ) << 8 ) | ( p[4] >> 0 ); } if( sx0 ) { sx0[0] = ( p[5] & 0xf0 ) >> 4; } if( sy0 ) { sy0[0] = ( p[5] & 0x0f ) >> 0; } } static inline void RFIFOPOS(int fd, unsigned short pos, short* x, short* y, unsigned char* dir) { RBUFPOS(RFIFOP(fd,pos), 0, x, y, dir); } static inline void RFIFOPOS2(int fd, unsigned short pos, short* x0, short* y0, short* x1, short* y1, unsigned char* sx0, unsigned char* sy0) { RBUFPOS2(WFIFOP(fd,pos), 0, x0, y0, x1, y1, sx0, sy0); } //To idenfity disguised characters. static inline bool disguised(struct block_list* bl) { return (bool)( bl->type == BL_PC && ((TBL_PC*)bl)->disguise ); } //Guarantees that the given string does not exceeds the allowed size, as well as making sure it's null terminated. [Skotlex] static inline unsigned int mes_len_check(char* mes, unsigned int len, unsigned int max) { if( len > max ) len = max; mes[len-1] = '\0'; return len; } static char map_ip_str[128]; static uint32 map_ip; static uint32 bind_ip = INADDR_ANY; static uint16 map_port = 5121; static bool clif_ally_only = false; int map_fd; static int clif_parse (int fd); /*========================================== * Ip setting of map-server *------------------------------------------*/ int clif_setip(const char* ip) { char ip_str[16]; map_ip = host2ip(ip); if (!map_ip) { ShowWarning("Failed to Resolve Map Server Address! (%s)\n", ip); return 0; } safestrncpy(map_ip_str, ip, sizeof(map_ip_str)); ShowInfo("Map Server IP Address : '" CL_WHITE "%s" CL_RESET "' -> '" CL_WHITE "%s" CL_RESET "'.\n", ip, ip2str(map_ip, ip_str)); return 1; } void clif_setbindip(const char* ip) { bind_ip = host2ip(ip); if (bind_ip) { char ip_str[16]; ShowInfo("Map Server Bind IP Address : '" CL_WHITE "%s" CL_RESET "' -> '" CL_WHITE "%s" CL_RESET "'.\n", ip, ip2str(bind_ip, ip_str)); } else { ShowWarning("Failed to Resolve Map Server Address! (%s)\n", ip); } } /*========================================== * Sets map port to 'port' * is run from map.cpp upon loading map server configuration *------------------------------------------*/ void clif_setport(uint16 port) { map_port = port; } /*========================================== * Returns map server IP *------------------------------------------*/ uint32 clif_getip(void) { return map_ip; } //Refreshes map_server ip, returns the new ip if the ip changed, otherwise it returns 0. uint32 clif_refresh_ip(void) { uint32 new_ip; new_ip = host2ip(map_ip_str); if (new_ip && new_ip != map_ip) { map_ip = new_ip; ShowInfo("Updating IP resolution of [%s].\n", map_ip_str); return map_ip; } return 0; } /*========================================== * Returns map port which is set by clif_setport() *------------------------------------------*/ uint16 clif_getport(void) { return map_port; } #if PACKETVER >= 20071106 static inline unsigned char clif_bl_type(struct block_list *bl, bool walking) { switch (bl->type) { case BL_PC: return (disguised(bl) && !pcdb_checkid(status_get_viewdata(bl)->class_))? 0x1:0x0; //PC_TYPE case BL_ITEM: return 0x2; //ITEM_TYPE case BL_SKILL: return 0x3; //SKILL_TYPE case BL_CHAT: return 0x4; //UNKNOWN_TYPE case BL_MOB: if( pcdb_checkid( status_get_viewdata( bl )->class_ ) ){ return 0x0; //PC_TYPE }else{ switch( ( (mob_data*)bl )->special_state.ai ){ case AI_ABR: return 0xd; //NPC_ABR_TYPE case AI_BIONIC: return 0xe; //NPC_BIONIC_TYPE default: return 0x5; //NPC_MOB_TYPE } } case BL_NPC: // From 2017-07-26 on NPC type units can also use player sprites. // There is one exception and this is if they are walking. // Since walking NPCs are not supported on official servers, the client does not know how to handle it. #if PACKETVER >= 20170726 if (pcdb_checkid( status_get_viewdata( bl )->class_ ) && walking) return 0x0; else if (mobdb_checkid( status_get_viewdata( bl )->class_ )) // FIXME: categorize NPCs able to walk return 0xC; else return 0x6; #else return pcdb_checkid(status_get_viewdata(bl)->class_) ? 0x0 : 0x6; //NPC_EVT_TYPE #endif case BL_PET: return pcdb_checkid(status_get_viewdata(bl)->class_)?0x0:0x7; //NPC_PET_TYPE case BL_HOM: return 0x8; //NPC_HOM_TYPE case BL_MER: return 0x9; //NPC_MERSOL_TYPE case BL_ELEM: return 0xa; //NPC_ELEMENTAL_TYPE default: return 0x1; //NPC_TYPE } } #endif static bool clif_session_isValid(map_session_data *sd) { return ( sd != nullptr && session_isActive(sd->fd) ); } /*========================================== * sub process of clif_send * Called from a map_foreachinallarea (grabs all players in specific area and subjects them to this function) * In order to send area-wise packets, such as: * - AREA : everyone nearby your area * - AREA_WOSC (AREA WITHOUT SAME CHAT) : Not run for people in the same chat as yours * - AREA_WOC (AREA WITHOUT CHAT) : Not run for people inside a chat * - AREA_WOS (AREA WITHOUT SELF) : Not run for self * - AREA_CHAT_WOC : Everyone in the area of your chat without a chat *------------------------------------------*/ static int clif_send_sub(struct block_list *bl, va_list ap) { struct block_list *src_bl; map_session_data *sd; unsigned char *buf; int len, type, fd; nullpo_ret(bl); nullpo_ret(sd = (map_session_data *)bl); // Don't send to disconnected clients. if( !session_isActive( fd = sd->fd ) ){ return 0; } buf = va_arg(ap,unsigned char*); len = va_arg(ap,int); nullpo_ret(src_bl = va_arg(ap,struct block_list*)); type = va_arg(ap,int); switch(type) { case AREA_WOS: if (bl == src_bl) return 0; break; case AREA_WOC: if (sd->chatID || bl == src_bl) return 0; break; case AREA_WOSC: { if(src_bl->type == BL_PC) { map_session_data *ssd = (map_session_data *)src_bl; if (ssd && sd->chatID && (sd->chatID == ssd->chatID)) return 0; } else if(src_bl->type == BL_NPC) { struct npc_data *nd = (struct npc_data *)src_bl; if (nd && sd->chatID && (sd->chatID == nd->chat_id)) return 0; } } break; } if( src_bl->type == BL_NPC && npc_is_hidden_dynamicnpc( *( (struct npc_data*)src_bl ), *sd ) ){ // Do not send anything return 0; } /* unless visible, hold it here */ if (!battle_config.update_enemy_position && clif_ally_only && !sd->special_state.intravision && !sd->sc.getSCE(SC_INTRAVISION) && battle_check_target(src_bl,&sd->bl,BCT_ENEMY) > 0) return 0; WFIFOHEAD(fd, len); if (WFIFOP(fd,0) == buf) { ShowError("WARNING: Invalid use of clif_send function\n"); ShowError(" Packet x%4x use a WFIFO of a player instead of to use a buffer.\n", WBUFW(buf,0)); ShowError(" Please correct your code.\n"); // don't send to not move the pointer of the packet for next sessions in the loop //WFIFOSET(fd,0);//## TODO is this ok? //NO. It is not ok. There is the chance WFIFOSET actually sends the buffer data, and shifts elements around, which will corrupt the buffer. return 0; } memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); return 0; } /*========================================== * Packet Delegation (called on all packets that require data to be sent to more than one client) * functions that are sent solely to one use whose ID it posses use WFIFOSET *------------------------------------------*/ int clif_send(const void* buf, int len, struct block_list* bl, enum send_target type) { int i; map_session_data *sd, *tsd; struct party_data *p = nullptr; std::shared_ptr bg; int x0 = 0, x1 = 0, y0 = 0, y1 = 0, fd; struct s_mapiterator* iter; if( type != ALL_CLIENT ) nullpo_ret(bl); sd = BL_CAST(BL_PC, bl); switch(type) { case ALL_CLIENT: //All player clients. iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( session_isActive( fd = tsd->fd ) ){ WFIFOHEAD( fd, len ); memcpy( WFIFOP( fd, 0 ), buf, len ); WFIFOSET( fd, len ); } } mapit_free(iter); break; case ALL_SAMEMAP: //All players on the same map iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( bl->m == tsd->bl.m && session_isActive( fd = tsd->fd ) ){ WFIFOHEAD( fd, len ); memcpy( WFIFOP( fd, 0 ), buf, len ); WFIFOSET( fd, len ); } } mapit_free(iter); break; case AREA: case AREA_WOSC: if (sd && bl->prev == nullptr) //Otherwise source misses the packet.[Skotlex] clif_send (buf, len, bl, SELF); [[fallthrough]]; case AREA_WOC: case AREA_WOS: map_foreachinallarea(clif_send_sub, bl->m, bl->x-AREA_SIZE, bl->y-AREA_SIZE, bl->x+AREA_SIZE, bl->y+AREA_SIZE, BL_PC, buf, len, bl, type); break; case AREA_CHAT_WOC: map_foreachinallarea(clif_send_sub, bl->m, bl->x-(AREA_SIZE-5), bl->y-(AREA_SIZE-5), bl->x+(AREA_SIZE-5), bl->y+(AREA_SIZE-5), BL_PC, buf, len, bl, AREA_WOC); break; case CHAT: case CHAT_WOS: { struct chat_data *cd; if (sd) { cd = (struct chat_data*)map_id2bl(sd->chatID); } else if (bl->type == BL_CHAT) { cd = (struct chat_data*)bl; } else break; if (cd == nullptr) break; for(i = 0; i < cd->users; i++) { if (type == CHAT_WOS && cd->usersd[i] == sd) continue; if( session_isActive( fd = cd->usersd[i]->fd ) ){ WFIFOHEAD(fd,len); memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); } } } break; case PARTY_AREA: case PARTY_AREA_WOS: x0 = bl->x - AREA_SIZE; y0 = bl->y - AREA_SIZE; x1 = bl->x + AREA_SIZE; y1 = bl->y + AREA_SIZE; [[fallthrough]]; case PARTY: case PARTY_WOS: case PARTY_SAMEMAP: case PARTY_SAMEMAP_WOS: if (sd && sd->status.party_id) p = party_search(sd->status.party_id); if (p) { for(i=0;idata[i].sd) == nullptr ) continue; if( !session_isActive( fd = sd->fd ) ) continue; if( sd->bl.id == bl->id && (type == PARTY_WOS || type == PARTY_SAMEMAP_WOS || type == PARTY_AREA_WOS) ) continue; if( type != PARTY && type != PARTY_WOS && bl->m != sd->bl.m ) continue; if( (type == PARTY_AREA || type == PARTY_AREA_WOS) && (sd->bl.x < x0 || sd->bl.y < y0 || sd->bl.x > x1 || sd->bl.y > y1) ) continue; WFIFOHEAD(fd, len); memcpy(WFIFOP(fd, 0), buf, len); WFIFOSET(fd, len); } if (!enable_spy) //Skip unnecessary parsing. [Skotlex] break; iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( tsd->partyspy == p->party.party_id && session_isActive( fd = tsd->fd ) ){ WFIFOHEAD( fd, len ); memcpy( WFIFOP( fd, 0 ), buf, len ); WFIFOSET( tsd->fd, len ); } } mapit_free(iter); } break; case DUEL: case DUEL_WOS: if (!sd || !sd->duel_group) break; //Invalid usage. iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( type == DUEL_WOS && bl->id == tsd->bl.id ) continue; if( sd->duel_group == tsd->duel_group && session_isActive( fd = tsd->fd ) ){ WFIFOHEAD( fd, len ); memcpy( WFIFOP( fd, 0 ), buf, len ); WFIFOSET( fd, len ); } } mapit_free(iter); break; case SELF: if( clif_session_isValid(sd) ){ fd = sd->fd; WFIFOHEAD(fd,len); memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); } break; // New definitions for guilds [Valaris] - Cleaned up and reorganized by [Skotlex] case GUILD_AREA: case GUILD_AREA_WOS: x0 = bl->x - AREA_SIZE; y0 = bl->y - AREA_SIZE; x1 = bl->x + AREA_SIZE; y1 = bl->y + AREA_SIZE; [[fallthrough]]; case GUILD_SAMEMAP: case GUILD_SAMEMAP_WOS: case GUILD: case GUILD_WOS: case GUILD_NOBG: { if (!sd || !sd->status.guild_id || !sd->guild) break; const auto &g = sd->guild->guild; for(i = 0; i < g.max_member; i++) { if( (sd = g.member[i].sd) != nullptr ){ if( !session_isActive( fd = sd->fd ) ) continue; if( type == GUILD_NOBG && sd->bg_id ) continue; if( sd->bl.id == bl->id && (type == GUILD_WOS || type == GUILD_SAMEMAP_WOS || type == GUILD_AREA_WOS) ) continue; if( type != GUILD && type != GUILD_NOBG && type != GUILD_WOS && sd->bl.m != bl->m ) continue; if( (type == GUILD_AREA || type == GUILD_AREA_WOS) && (sd->bl.x < x0 || sd->bl.y < y0 || sd->bl.x > x1 || sd->bl.y > y1) ) continue; WFIFOHEAD(fd,len); memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); } } if (!enable_spy) //Skip unnecessary parsing. [Skotlex] break; iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( tsd->guildspy == g.guild_id && session_isActive( fd = tsd->fd ) ){ WFIFOHEAD( fd, len ); memcpy( WFIFOP( fd, 0 ), buf, len ); WFIFOSET( fd, len ); } } mapit_free(iter); break; } case BG_AREA: case BG_AREA_WOS: x0 = bl->x - AREA_SIZE; y0 = bl->y - AREA_SIZE; x1 = bl->x + AREA_SIZE; y1 = bl->y + AREA_SIZE; [[fallthrough]]; case BG_SAMEMAP: case BG_SAMEMAP_WOS: case BG: case BG_WOS: if( sd && sd->bg_id > 0 && (bg = util::umap_find(bg_team_db, sd->bg_id))) { for (const auto &member : bg->members) { if( ( sd = member.sd ) == nullptr || !session_isActive( fd = sd->fd ) ) continue; if(sd->bl.id == bl->id && (type == BG_WOS || type == BG_SAMEMAP_WOS || type == BG_AREA_WOS) ) continue; if( type != BG && type != BG_WOS && sd->bl.m != bl->m ) continue; if( (type == BG_AREA || type == BG_AREA_WOS) && (sd->bl.x < x0 || sd->bl.y < y0 || sd->bl.x > x1 || sd->bl.y > y1) ) continue; WFIFOHEAD(fd,len); memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); } } break; case CLAN: if( sd && sd->clan ){ struct clan* clan = sd->clan; for( i = 0; i < clan->max_member; i++ ){ if( ( sd = clan->members[i] ) == nullptr || !session_isActive( fd = sd->fd ) ){ continue; } WFIFOHEAD(fd,len); memcpy(WFIFOP(fd,0), buf, len); WFIFOSET(fd,len); } if (!enable_spy) //Skip unnecessary parsing. [Skotlex] break; iter = mapit_getallusers(); while( ( tsd = (map_session_data*)mapit_next( iter ) ) != nullptr ){ if( tsd->clanspy == clan->id && session_isActive( fd = tsd->fd ) ){ WFIFOHEAD(fd, len); memcpy(WFIFOP(fd, 0), buf, len); WFIFOSET(fd, len); } } mapit_free(iter); } break; default: ShowError("clif_send: Unrecognized type %d\n",type); return -1; } return 0; } /// Notifies the client, that it's connection attempt was accepted. /// 0073 .L .3B .B .B (ZC_ACCEPT_ENTER) /// 02eb .L .3B .B .B .W (ZC_ACCEPT_ENTER2) /// 0a18 .L .3B .B .B .W .B (ZC_ACCEPT_ENTER3) void clif_authok(map_session_data *sd) { PACKET_ZC_ACCEPT_ENTER packet{}; packet.packetType = HEADER_ZC_ACCEPT_ENTER; packet.startTime = client_tick(gettick()); WBUFPOS(packet.posDir, 0, sd->bl.x, sd->bl.y, sd->ud.dir); packet.xSize = 5; // ignored packet.ySize = 5; // ignored #if PACKETVER >= 20080102 packet.font = sd->status.font; #endif #if PACKETVER >= 20141016 && PACKETVER < 20160330 packet.sex = sd->status.sex; #endif clif_send(&packet, sizeof(packet), &sd->bl, SELF); } /// Notifies the client, that it's connection attempt was refused. /// 0074 .B (ZC_REFUSE_ENTER) /// error code: /// 0 = client type mismatch /// 1 = ID mismatch /// 2 = mobile - out of available time /// 3 = mobile - already logged in /// 4 = mobile - waiting state void clif_authrefuse(int fd, uint8 error_code) { PACKET_ZC_REFUSE_ENTER packet{}; packet.packetType = HEADER_ZC_REFUSE_ENTER; packet.errorCode = error_code; socket_send(fd, packet); } /// Notifies the client of a ban or forced disconnect. /// 0081 .B (SC_NOTIFY_BAN) /// error code: /// 0 = BAN_UNFAIR /// 1 = server closed -> MsgStringTable[4] /// 2 = ID already logged in -> MsgStringTable[5] /// 3 = timeout/too much lag -> MsgStringTable[241] /// 4 = server full -> MsgStringTable[264] /// 5 = underaged -> MsgStringTable[305] /// 8 = Server sill recognizes last connection -> MsgStringTable[441] /// 9 = too many connections from this ip -> MsgStringTable[529] /// 10 = out of available time paid for -> MsgStringTable[530] /// 11 = BAN_PAY_SUSPEND /// 12 = BAN_PAY_CHANGE /// 13 = BAN_PAY_WRONGIP /// 14 = BAN_PAY_PNGAMEROOM /// 15 = disconnected by a GM -> if( servicetype == taiwan ) MsgStringTable[579] /// 16 = BAN_JAPAN_REFUSE1 /// 17 = BAN_JAPAN_REFUSE2 /// 18 = BAN_INFORMATION_REMAINED_ANOTHER_ACCOUNT /// 100 = BAN_PC_IP_UNFAIR /// 101 = BAN_PC_IP_COUNT_ALL /// 102 = BAN_PC_IP_COUNT /// 103 = BAN_GRAVITY_MEM_AGREE /// 104 = BAN_GAME_MEM_AGREE /// 105 = BAN_HAN_VALID /// 106 = BAN_PC_IP_LIMIT_ACCESS /// 107 = BAN_OVER_CHARACTER_LIST /// 108 = BAN_IP_BLOCK /// 109 = BAN_INVALID_PWD_CNT /// 110 = BAN_NOT_ALLOWED_JOBCLASS /// ? = disconnected -> MsgStringTable[3] void clif_authfail_fd(int fd, int type) { if (!session_isValid(fd) || session[fd]->func_parse != clif_parse) //clif_authfail should only be invoked on players! return; PACKET_SC_NOTIFY_BAN packet{}; packet.packetType = HEADER_SC_NOTIFY_BAN; packet.errorCode = static_cast(type); socket_send(fd, packet); set_eof(fd); } /// Notifies the client, whether it can disconnect and change servers. /// 00b3 .B (ZC_RESTART_ACK) /// type: /// 1 = disconnect, char-select /// ? = nothing void clif_charselectok(int id, uint8 ok) { map_session_data* sd = map_id2sd(id); if (sd == nullptr) return; PACKET_ZC_RESTART_ACK packet{}; packet.packetType = HEADER_ZC_RESTART_ACK; packet.type = ok; clif_send( &packet, sizeof( packet ), &sd->bl, SELF ); } /// Makes an item appear on the ground. /// 009E .L .W .B .W .W .B .B .W (ZC_ITEM_FALL_ENTRY) /// 084B .L .W .W .B .W .W .B .B .W (ZC_ITEM_FALL_ENTRY4) /// 0ADD .L .W .W .B .W .W .B .B .W .B .W (ZC_ITEM_FALL_ENTRY5) void clif_dropflooritem( struct flooritem_data* fitem, bool canShowEffect ){ nullpo_retv(fitem); if( fitem->item.nameid == 0 ){ return; } struct packet_dropflooritem p; p.PacketType = dropflooritemType; p.ITAID = fitem->bl.id; p.ITID = client_nameid( fitem->item.nameid ); #if PACKETVER >= 20130000 /* not sure date */ p.type = itemtype( fitem->item.nameid ); #endif p.IsIdentified = fitem->item.identify ? 1 : 0; p.xPos = fitem->bl.x; p.yPos = fitem->bl.y; p.subX = fitem->subx; p.subY = fitem->suby; p.count = fitem->item.amount; #if defined(PACKETVER_ZERO) || PACKETVER >= 20180418 if( canShowEffect ){ uint8 dropEffect = itemdb_dropeffect( fitem->item.nameid ); if( dropEffect > 0 ){ p.showdropeffect = 1; p.dropeffectmode = dropEffect - 1; }else if (battle_config.rndopt_drop_pillar != 0){ uint8 optionCount = 0; for (uint8 i = 0; i < MAX_ITEM_RDM_OPT; i++) { if (fitem->item.option[i].id != 0) { optionCount++; } } if (optionCount > 0) { p.showdropeffect = 1; if (optionCount == 1) p.dropeffectmode = DROPEFFECT_BLUE_PILLAR - 1; else if (optionCount == 2) p.dropeffectmode = DROPEFFECT_YELLOW_PILLAR - 1; else p.dropeffectmode = DROPEFFECT_PURPLE_PILLAR - 1; } else { p.showdropeffect = 0; p.dropeffectmode = DROPEFFECT_NONE; } } else { p.showdropeffect = 0; p.dropeffectmode = DROPEFFECT_NONE; } }else{ p.showdropeffect = 0; p.dropeffectmode = DROPEFFECT_NONE; } #endif clif_send( &p, sizeof(p), &fitem->bl, AREA ); } /// Makes an item disappear from the ground. /// 00a1 .L (ZC_ITEM_DISAPPEAR) void clif_clearflooritem( flooritem_data& fitem, map_session_data* tsd ){ PACKET_ZC_ITEM_DISAPPEAR packet{}; packet.packetType = HEADER_ZC_ITEM_DISAPPEAR; packet.itemAid = fitem.bl.id; if( tsd == nullptr ){ clif_send(&packet, sizeof(PACKET_ZC_ITEM_DISAPPEAR), &fitem.bl, AREA); } else { clif_send(&packet, sizeof(PACKET_ZC_ITEM_DISAPPEAR), &tsd->bl, SELF ); } } /// Makes a unit (char, npc, mob, homun) disappear to one client. /// 0080 .L .B (ZC_NOTIFY_VANISH) /// type: /// 0 = out of sight /// 1 = died /// 2 = logged out /// 3 = teleport /// 4 = trickdead void clif_clearunit_single( uint32 GID, clr_type type, map_session_data& tsd ){ PACKET_ZC_NOTIFY_VANISH packet{}; packet.packetType = HEADER_ZC_NOTIFY_VANISH; packet.gid = GID; packet.type = static_cast(type); clif_send( &packet, sizeof( packet ), &tsd.bl, SELF ); } /// Makes a unit (char, npc, mob, homun) disappear to all clients in area. /// 0080 .L .B (ZC_NOTIFY_VANISH) /// type: /// 0 = out of sight /// 1 = died /// 2 = logged out /// 3 = teleport /// 4 = trickdead void clif_clearunit_area( block_list& bl, clr_type type ){ PACKET_ZC_NOTIFY_VANISH packet{}; packet.packetType = HEADER_ZC_NOTIFY_VANISH; packet.gid = bl.id; packet.type = static_cast(type); clif_send(&packet, sizeof(PACKET_ZC_NOTIFY_VANISH), &bl, type == CLR_DEAD ? AREA : AREA_WOS); if(disguised(&bl)) { packet.gid = disguised_bl_id( bl.id ); clif_send(&packet, sizeof(PACKET_ZC_NOTIFY_VANISH), &bl, SELF); } } /// Used to make monsters with player-sprites disappear after dying /// like normal monsters, because the client does not remove those /// automatically. static TIMER_FUNC(clif_clearunit_delayed_sub){ struct block_list *bl = (struct block_list *)data; if( bl != nullptr ){ clif_clearunit_area( *bl, (clr_type)id ); ers_free( delay_clearunit_ers, bl ); } return 0; } void clif_clearunit_delayed(struct block_list* bl, clr_type type, t_tick tick) { struct block_list *tbl = ers_alloc(delay_clearunit_ers, struct block_list); tbl->next = nullptr; tbl->prev = nullptr; tbl->id = bl->id; tbl->m = bl->m; tbl->x = bl->x; tbl->y = bl->y; tbl->type = BL_NUL; add_timer(tick, clif_clearunit_delayed_sub, (int)type, (intptr_t)tbl); } void clif_get_weapon_view(map_session_data* sd, t_itemid *rhand, t_itemid *lhand) { if(sd->sc.option&OPTION_COSTUME) { *rhand = *lhand = 0; return; } #if PACKETVER < 4 *rhand = sd->status.weapon; *lhand = sd->status.shield; #else if (sd->equip_index[EQI_HAND_R] >= 0 && sd->inventory_data[sd->equip_index[EQI_HAND_R]]) { struct item_data* id = sd->inventory_data[sd->equip_index[EQI_HAND_R]]; if (id->view_id > 0) *rhand = id->view_id; else *rhand = id->nameid; } else *rhand = 0; if (sd->equip_index[EQI_HAND_L] >= 0 && sd->equip_index[EQI_HAND_L] != sd->equip_index[EQI_HAND_R] && sd->inventory_data[sd->equip_index[EQI_HAND_L]]) { struct item_data* id = sd->inventory_data[sd->equip_index[EQI_HAND_L]]; if (id->view_id > 0) *lhand = id->view_id; else *lhand = id->nameid; } else *lhand = 0; #endif } //To make the assignation of the level based on limits clearer/easier. [Skotlex] static int clif_setlevel_sub(int lv) { if( lv < battle_config.max_lv ) { ; } else if( lv < battle_config.aura_lv ) { lv = battle_config.max_lv - 1; } else { lv = battle_config.max_lv; } return lv; } static int clif_setlevel(struct block_list* bl) { int lv = status_get_lv(bl); if( battle_config.client_limit_unit_lv&bl->type ) return clif_setlevel_sub(lv); switch( bl->type ) { case BL_NPC: case BL_PET: // npcs and pets do not have level return 0; } return lv; } /*========================================== * Prepares 'unit standing/spawning' packet *------------------------------------------*/ static void clif_set_unit_idle( struct block_list* bl, bool walking, send_target target, struct block_list* tbl ){ nullpo_retv( bl ); map_session_data* sd = BL_CAST( BL_PC, bl ); status_change* sc = status_get_sc( bl ); struct view_data* vd = status_get_viewdata( bl ); int g_id = status_get_guild_id( bl ); #if PACKETVER < 20091103 if( !pcdb_checkid( vd->class_ ) ){ struct packet_idle_unit2 p; p.PacketType = idle_unit2Type; #if PACKETVER >= 20071106 p.objecttype = clif_bl_type( bl, walking ); #endif p.GID = bl->id; p.speed = status_get_speed( bl ); p.bodyState = ( sc ) ? sc->opt1 : 0; p.healthState = ( sc ) ? sc->opt2 : 0; p.effectState = ( sc ) ? sc->option : 0; p.job = vd->class_; p.head = vd->hair_style; p.weapon = vd->weapon; p.accessory = vd->head_bottom; if( bl->type == BL_NPC && vd->class_ == JT_GUILD_FLAG ){ // The hell, why flags work like this? p.shield = status_get_emblem_id( bl ); p.accessory2 = GetWord( g_id, 1 ); p.accessory3 = GetWord( g_id, 0 ); }else{ p.shield = vd->shield; p.accessory2 = vd->head_top; p.accessory3 = vd->head_mid; } p.headpalette = vd->hair_color; p.bodypalette = vd->cloth_color; p.headDir = ( sd )? sd->head_dir : 0; p.GUID = g_id; p.GEmblemVer = status_get_emblem_id( bl ); p.honor = ( sd ) ? sd->status.manner : 0; p.virtue = ( sc ) ? sc->opt3 : 0; p.isPKModeON = ( sd && sd->status.karma ) ? 1 : 0; p.sex = vd->sex; WBUFPOS( &p.PosDir[0], 0, bl->x, bl->y, unit_getdir( bl ) ); p.xSize = p.ySize = ( sd ) ? 5 : 0; p.state = vd->dead_sit; p.clevel = clif_setlevel( bl ); clif_send( &p, sizeof( p ), tbl, target ); return; } #endif struct packet_idle_unit p; p.PacketType = idle_unitType; #if PACKETVER >= 20091103 p.PacketLength = sizeof(p); p.objecttype = clif_bl_type( bl, walking ); #endif #if PACKETVER >= 20131223 p.AID = bl->id; p.GID = (sd) ? sd->status.char_id : 0; // CCODE #else p.GID = bl->id; #endif p.speed = status_get_speed(bl); p.bodyState = (sc) ? sc->opt1 : 0; p.healthState = (sc) ? sc->opt2 : 0; // npc option changed? if( tbl && tbl->type == BL_PC && bl->type == BL_NPC ){ map_session_data* sd = (map_session_data*)tbl; struct npc_data* nd = (struct npc_data*)bl; int option = (sc) ? sc->option : 0; if( !nd->vd.dead_sit ){ if( std::find( sd->cloaked_npc.begin(), sd->cloaked_npc.end(), nd->bl.id ) != sd->cloaked_npc.end() ){ option ^= OPTION_CLOAK; } } p.effectState = option; }else{ p.effectState = (sc) ? sc->option : 0; } p.job = vd->class_; p.head = vd->hair_style; p.weapon = vd->weapon; #if PACKETVER < 7 || PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 p.shield = vd->shield; #endif if( bl->type == BL_NPC && vd->class_ == JT_GUILD_FLAG ){ // The hell, why flags work like this? p.accessory = status_get_emblem_id( bl ); p.accessory2 = GetWord( g_id, 1 ); p.accessory3 = GetWord( g_id, 0 ); }else{ p.accessory = vd->head_bottom; p.accessory2 = vd->head_top; p.accessory3 = vd->head_mid; } p.headpalette = vd->hair_color; p.bodypalette = vd->cloth_color; p.headDir = (sd)? sd->head_dir : 0; #if PACKETVER >= 20101124 p.robe = vd->robe; #endif p.GUID = g_id; p.GEmblemVer = status_get_emblem_id( bl ); p.honor = (sd) ? sd->status.manner : 0; p.virtue = (sc) ? sc->opt3 : 0; p.isPKModeON = (sd && sd->status.karma) ? 1 : 0; p.sex = vd->sex; WBUFPOS( &p.PosDir[0], 0, bl->x, bl->y, unit_getdir( bl ) ); p.xSize = p.ySize = (sd) ? 5 : 0; p.state = vd->dead_sit; p.clevel = clif_setlevel( bl ); #if PACKETVER >= 20080102 p.font = (sd) ? sd->status.font : 0; #endif #if PACKETVER >= 20120221 if( battle_config.monster_hp_bars_info && !map_getmapflag( bl->m, MF_HIDEMOBHPBAR ) && bl->type == BL_MOB && ( status_get_hp( bl ) < status_get_max_hp( bl ) ) ){ p.maxHP = status_get_max_hp(bl); p.HP = status_get_hp(bl); }else{ p.maxHP = -1; p.HP = -1; } if( bl->type == BL_MOB ){ p.isBoss = ( (mob_data*)bl )->get_bosstype(); }else if( bl->type == BL_PET ){ p.isBoss = ( (pet_data*)bl )->db->get_bosstype(); }else{ p.isBoss = BOSSTYPE_NONE; } #endif #if PACKETVER >= 20150513 p.body = vd->body_style; #endif /* Might be earlier, this is when the named item bug began */ #if PACKETVER >= 20131223 safestrncpy(p.name, status_get_name( bl ), NAME_LENGTH); #endif clif_send( &p, sizeof( p ), tbl, target ); // if disguised, send to self if( disguised( bl ) ){ #if PACKETVER >= 20091103 p.objecttype = pcdb_checkid( status_get_viewdata( bl )->class_ ) ? 0x0 : 0x5; //PC_TYPE : NPC_MOB_TYPE #if PACKETVER >= 20131223 p.AID = disguised_bl_id( bl->id ); #else p.GID = disguised_bl_id( bl->id ); #endif #else p.GID = disguised_bl_id( bl->id ); #endif clif_send(&p, sizeof(p), bl, SELF); } } static void clif_spawn_unit( struct block_list *bl, enum send_target target ){ nullpo_retv( bl ); map_session_data* sd = BL_CAST( BL_PC, bl ); status_change* sc = status_get_sc( bl ); struct view_data* vd = status_get_viewdata( bl ); int g_id = status_get_guild_id( bl ); #if PACKETVER < 20091103 if( !pcdb_checkid( vd->class_ ) ){ struct packet_spawn_unit2 p; p.PacketType = spawn_unit2Type; #if PACKETVER >= 20071106 p.objecttype = clif_bl_type( bl, false ); #endif p.GID = bl->id; p.speed = status_get_speed( bl ); p.bodyState = ( sc ) ? sc->opt1 : 0; p.healthState = ( sc ) ? sc->opt2 : 0; p.effectState = ( sc ) ? sc->option : 0; p.head = vd->hair_style; p.weapon = vd->weapon; p.accessory = vd->head_bottom; p.job = vd->class_; if( bl->type == BL_NPC && vd->class_ == JT_GUILD_FLAG ){ // The hell, why flags work like this? p.shield = status_get_emblem_id( bl ); p.accessory2 = GetWord( g_id, 1 ); p.accessory3 = GetWord( g_id, 0 ); }else{ p.shield = vd->shield; p.accessory2 = vd->head_top; p.accessory3 = vd->head_mid; } p.headpalette = vd->hair_color; p.bodypalette = vd->cloth_color; p.headDir = ( sd ) ? sd->head_dir : 0; p.isPKModeON = ( sd && sd->status.karma ) ? 1 : 0; p.sex = vd->sex; WBUFPOS( &p.PosDir[0], 0, bl->x, bl->y, unit_getdir( bl ) ); p.xSize = p.ySize = ( sd ) ? 5 : 0; clif_send( &p, sizeof( p ), bl, target ); return; } #endif struct packet_spawn_unit p; p.PacketType = spawn_unitType; #if PACKETVER >= 20091103 p.PacketLength = sizeof(p); p.objecttype = clif_bl_type( bl, false ); #endif #if PACKETVER >= 20131223 p.AID = bl->id; p.GID = (sd) ? sd->status.char_id : 0; // CCODE #else p.GID = bl->id; #endif p.speed = status_get_speed( bl ); p.bodyState = (sc) ? sc->opt1 : 0; p.healthState = (sc) ? sc->opt2 : 0; p.effectState = (sc) ? sc->option : 0; p.job = vd->class_; p.head = vd->hair_style; p.weapon = vd->weapon; #if PACKETVER < 7 || PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 p.shield = vd->shield; #endif if( bl->type == BL_NPC && vd->class_ == JT_GUILD_FLAG ){ // The hell, why flags work like this? p.accessory = status_get_emblem_id( bl ); p.accessory2 = GetWord( g_id, 1 ); p.accessory3 = GetWord( g_id, 0 ); }else{ p.accessory = vd->head_bottom; p.accessory2 = vd->head_top; p.accessory3 = vd->head_mid; } p.headpalette = vd->hair_color; p.bodypalette = vd->cloth_color; p.headDir = (sd)? sd->head_dir : 0; #if PACKETVER >= 20101124 p.robe = vd->robe; #endif p.GUID = g_id; p.GEmblemVer = status_get_emblem_id( bl ); p.honor = (sd) ? sd->status.manner : 0; p.virtue = (sc) ? sc->opt3 : 0; p.isPKModeON = (sd && sd->status.karma) ? 1 : 0; p.sex = vd->sex; WBUFPOS( &p.PosDir[0], 0, bl->x, bl->y, unit_getdir( bl ) ); p.xSize = p.ySize = (sd) ? 5 : 0; p.clevel = clif_setlevel( bl ); #if PACKETVER >= 20080102 p.font = (sd) ? sd->status.font : 0; #endif #if PACKETVER >= 20120221 if( battle_config.monster_hp_bars_info && bl->type == BL_MOB && !map_getmapflag( bl->m, MF_HIDEMOBHPBAR ) && ( status_get_hp( bl ) < status_get_max_hp( bl ) ) ){ p.maxHP = status_get_max_hp( bl ); p.HP = status_get_hp( bl ); }else{ p.maxHP = -1; p.HP = -1; } if( bl->type == BL_MOB ){ p.isBoss = ( (mob_data*)bl )->get_bosstype(); }else if( bl->type == BL_PET ){ p.isBoss = ( (pet_data*)bl )->db->get_bosstype(); }else{ p.isBoss = BOSSTYPE_NONE; } #endif #if PACKETVER >= 20150513 p.body = vd->body_style; #endif /* Might be earlier, this is when the named item bug began */ #if PACKETVER >= 20131223 safestrncpy( p.name, status_get_name( bl ), NAME_LENGTH ); #endif if( disguised( bl ) ){ nullpo_retv( sd ); if( sd->status.class_ != sd->disguise ){ clif_send( &p, sizeof( p ), bl, target ); } #if PACKETVER >= 20091103 p.objecttype = pcdb_checkid( status_get_viewdata(bl)->class_ ) ? 0x0 : 0x5; //PC_TYPE : NPC_MOB_TYPE #if PACKETVER >= 20131223 p.AID = disguised_bl_id( bl->id ); #else p.GID = disguised_bl_id( bl->id ); #endif #else p.GID = disguised_bl_id( bl->id ); #endif clif_send( &p, sizeof( p ), bl, SELF ); }else{ clif_send( &p, sizeof( p ), bl, target ); } } /*========================================== * Prepares 'unit walking' packet *------------------------------------------*/ static void clif_set_unit_walking( struct block_list& bl, map_session_data* tsd, struct unit_data& ud, enum send_target target ){ struct packet_unit_walking p; p.PacketType = unit_walkingType; #if PACKETVER >= 20091103 p.PacketLength = sizeof(p); #endif #if PACKETVER >= 20071106 p.objecttype = clif_bl_type( &bl, true ); #endif map_session_data* sd = BL_CAST(BL_PC, &bl); #if PACKETVER >= 20131223 p.AID = bl.id; p.GID = (sd) ? sd->status.char_id : 0; // CCODE #else p.GID = bl.id; #endif p.speed = status_get_speed( &bl ); status_change* sc = status_get_sc( &bl ); p.bodyState = (sc) ? sc->opt1 : 0; p.healthState = (sc) ? sc->opt2 : 0; p.effectState = (sc) ? sc->option : 0; struct view_data* vd = status_get_viewdata( &bl ); p.job = vd->class_; p.head = vd->hair_style; p.weapon = vd->weapon; p.accessory = vd->head_bottom; p.moveStartTime = client_tick(gettick()); #if PACKETVER < 7 || PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 p.shield = vd->shield; #endif p.accessory2 = vd->head_top; p.accessory3 = vd->head_mid; p.headpalette = vd->hair_color; p.bodypalette = vd->cloth_color; p.headDir = (sd) ? sd->head_dir : 0; #if PACKETVER >= 20101124 p.robe = vd->robe; #endif p.GUID = status_get_guild_id( &bl ); p.GEmblemVer = status_get_emblem_id( &bl ); p.honor = (sd) ? sd->status.manner : 0; p.virtue = (sc) ? sc->opt3 : 0; p.isPKModeON = (sd && sd->status.karma) ? 1 : 0; p.sex = vd->sex; WBUFPOS2( &p.MoveData[0], 0, bl.x, bl.y, ud.to_x, ud.to_y, 8, 8 ); p.xSize = p.ySize = (sd) ? 5 : 0; p.clevel = clif_setlevel( &bl ); #if PACKETVER >= 20080102 p.font = (sd) ? sd->status.font : 0; #endif #if PACKETVER >= 20120221 if( battle_config.monster_hp_bars_info && !map_getmapflag(bl.m, MF_HIDEMOBHPBAR) && bl.type == BL_MOB && (status_get_hp( &bl ) < status_get_max_hp( &bl ) ) ){ p.maxHP = status_get_max_hp( &bl ); p.HP = status_get_hp( &bl ); } else { p.maxHP = -1; p.HP = -1; } if( bl.type == BL_MOB ){ p.isBoss = reinterpret_cast( &bl )->get_bosstype(); }else if( bl.type == BL_PET ){ p.isBoss = reinterpret_cast( &bl )->db->get_bosstype(); }else{ p.isBoss = BOSSTYPE_NONE; } #endif #if PACKETVER >= 20150513 p.body = vd->body_style; #endif /* Might be earlier, this is when the named item bug began */ #if PACKETVER >= 20131223 safestrncpy(p.name, status_get_name( &bl ), NAME_LENGTH); #endif clif_send( &p, sizeof(p), tsd ? &tsd->bl : &bl, target ); // if disguised, send the info to self if( disguised( &bl ) ){ #if PACKETVER >= 20091103 p.objecttype = pcdb_checkid( status_get_viewdata( &bl )->class_ ) ? 0x0 : 0x5; //PC_TYPE : NPC_MOB_TYPE #if PACKETVER >= 20131223 p.AID = disguised_bl_id( bl.id ); #else p.GID = disguised_bl_id( bl.id ); #endif #else p.GID = disguised_bl_id( bl.id ); #endif clif_send(&p, sizeof(p), &bl, SELF); } } /// Changes sprite of an NPC object (ZC_NPCSPRITE_CHANGE). /// 01b0 .L .B .L /// type: /// unused void clif_class_change_target(struct block_list *bl,int class_,int type, enum send_target target, map_session_data *sd) { nullpo_retv(bl); if(!pcdb_checkid(class_)) {// player classes yield missing sprites unsigned char buf[16]; WBUFW(buf,0)=0x1b0; WBUFL(buf,2)=bl->id; WBUFB(buf,6)=type; WBUFL(buf,7)=class_; clif_send(buf,packet_len(0x1b0),(sd == nullptr ? bl : &(sd->bl)),target); } } void clif_servantball( map_session_data& sd, struct block_list* target, enum send_target send_target ){ struct PACKET_ZC_SPIRITS p = {}; p.PacketType = HEADER_ZC_SPIRITS; p.AID = sd.bl.id; p.num = sd.servantball; if( target == nullptr ){ target = &sd.bl; } clif_send( &p, sizeof( p ), target, send_target ); } void clif_abyssball( map_session_data& sd, struct block_list* target, enum send_target send_target ){ struct PACKET_ZC_SPIRITS p = {}; p.PacketType = HEADER_ZC_SPIRITS; p.AID = sd.bl.id; p.num = sd.abyssball; if( target == nullptr ){ target = &sd.bl; } clif_send( &p, sizeof( p ), target, send_target ); } /// Notifies the client of an object's Millenium Shields. static void clif_millenniumshield_single( map_session_data& sd, map_session_data& tsd ){ #if PACKETVER >= 20081126 status_change_entry* sce = sd.sc.getSCE( SC_MILLENNIUMSHIELD ); if( sce == nullptr ){ return; } PACKET_ZC_MILLENNIUMSHIELD packet{}; packet.packetType = HEADER_ZC_MILLENNIUMSHIELD; packet.aid = sd.bl.id; packet.num = sce->val2; packet.state = 0; clif_send( &packet, sizeof( packet ), &tsd.bl, SELF ); #endif } /*========================================== * Kagerou/Oboro amulet spirit *------------------------------------------*/ static void clif_spiritcharm_single( map_session_data& sd, map_session_data& tsd ){ #if PACKETVER >= 20111102 if( sd.spiritcharm_type == CHARM_TYPE_NONE ){ return; } if( sd.spiritcharm <= 0 ){ return; } PACKET_ZC_SPIRITS_ATTRIBUTE packet{}; packet.packetType = HEADER_ZC_SPIRITS_ATTRIBUTE; packet.aid = sd.bl.id; packet.spiritsType = static_cast(sd.spiritcharm_type); packet.num = static_cast(sd.spiritcharm); clif_send( &packet, sizeof( packet ), &tsd.bl, SELF ); #endif } /*========================================== * Enchanting Shadow / Shadow Scar Spirit *------------------------------------------*/ void clif_enchantingshadow_spirit(unit_data &ud) { #if PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191120 || PACKETVER_ZERO_NUM >= 20191127 PACKET_ZC_TARGET_SPIRITS p = {}; p.packetType = HEADER_ZC_TARGET_SPIRITS; p.GID = ud.bl->id; p.unknown_val = 0; p.amount = static_cast(ud.shadow_scar_timer.size()); clif_send(&p, sizeof(p), ud.bl, AREA); #endif } /*========================================== * Run when player changes map / refreshes * Tells its client to display all weather settings being used by this map *------------------------------------------*/ static void clif_weather_check(map_session_data *sd) { int16 m = sd->bl.m; int fd = sd->fd; if (map_getmapflag(m, MF_SNOW) || map_getmapflag(m, MF_CLOUDS) || map_getmapflag(m, MF_FOG) || map_getmapflag(m, MF_FIREWORKS) || map_getmapflag(m, MF_SAKURA) || map_getmapflag(m, MF_LEAVES) || map_getmapflag(m, MF_CLOUDS2)) { if (map_getmapflag(m, MF_SNOW)) clif_specialeffect_single(&sd->bl, EF_SNOW, fd); if (map_getmapflag(m, MF_CLOUDS)) clif_specialeffect_single(&sd->bl, EF_CLOUD3, fd); if (map_getmapflag(m, MF_CLOUDS2)) clif_specialeffect_single(&sd->bl, EF_CLOUD5, fd); if (map_getmapflag(m, MF_FOG)) clif_specialeffect_single(&sd->bl, EF_CLOUD4, fd); if (map_getmapflag(m, MF_FIREWORKS)) { clif_specialeffect_single(&sd->bl, EF_POKJUK, fd); clif_specialeffect_single(&sd->bl, EF_THROWITEM2, fd); clif_specialeffect_single(&sd->bl, EF_POKJUK_SOUND, fd); } if (map_getmapflag(m, MF_SAKURA)) clif_specialeffect_single(&sd->bl, EF_SAKURA, fd); if (map_getmapflag(m, MF_LEAVES)) clif_specialeffect_single(&sd->bl, EF_MAPLE, fd); } } /** * Run when the weather on a map changes, throws all players in map id 'm' to clif_weather_check function **/ void clif_weather(int16 m) { struct s_mapiterator* iter; map_session_data *sd=nullptr; iter = mapit_getallusers(); for( sd = (map_session_data*)mapit_first(iter); mapit_exists(iter); sd = (map_session_data*)mapit_next(iter) ) { if( sd->bl.m == m ) clif_weather_check(sd); } mapit_free(iter); } /** * Hide a NPC from the effects of Maya Purple card. * @param bl: Block data * @return True if NPC is disabled or false otherwise */ static inline bool clif_npc_mayapurple(block_list *bl) { nullpo_retr(false, bl); if (bl->type == BL_NPC) { npc_data *nd = map_id2nd(bl->id); // TODO: Confirm if waitingroom cause any special cases if (/* nd->chat_id == 0 && */ nd->is_invisible) return true; } return false; } /** * Main function to spawn a unit on the client (player/mob/pet/etc) **/ int clif_spawn( struct block_list *bl, bool walking ){ struct view_data *vd; vd = status_get_viewdata(bl); if( !vd || vd->class_ == JT_INVISIBLE ) return 0; // Hide NPC from Maya Purple card if (clif_npc_mayapurple(bl)) return 0; if( bl->type == BL_NPC && !vd->dead_sit ){ clif_set_unit_idle( bl, walking, AREA_WOS, bl ); }else{ clif_spawn_unit( bl, AREA_WOS ); } if (vd->cloth_color) clif_refreshlook(bl,bl->id,LOOK_CLOTHES_COLOR,vd->cloth_color,AREA_WOS); if (vd->body_style) clif_refreshlook(bl,bl->id,LOOK_BODY2,vd->body_style,AREA_WOS); switch (bl->type) { case BL_PC: { TBL_PC *sd = ((TBL_PC*)bl); if (sd->spiritball > 0) clif_spiritball(&sd->bl); if (sd->sc.getSCE(SC_MILLENNIUMSHIELD)) clif_millenniumshield(&sd->bl, sd->sc.getSCE(SC_MILLENNIUMSHIELD)->val2); if (sd->soulball > 0) clif_soulball(sd); if (sd->servantball > 0) clif_servantball( *sd ); if (sd->abyssball > 0) clif_abyssball( *sd ); if(sd->state.size==SZ_BIG) // tiny/big players [Valaris] clif_specialeffect(bl,EF_GIANTBODY2,AREA); else if(sd->state.size==SZ_MEDIUM) clif_specialeffect(bl,EF_BABYBODY2,AREA); if( sd->bg_id && map_getmapflag(sd->bl.m, MF_BATTLEGROUND) ) clif_sendbgemblem_area(sd); if (sd->spiritcharm_type != CHARM_TYPE_NONE && sd->spiritcharm > 0) clif_spiritcharm(sd); if (sd->status.robe) clif_refreshlook(bl,bl->id,LOOK_ROBE,sd->status.robe,AREA); clif_efst_status_change_sub(bl, bl, AREA); clif_hat_effects( *sd, sd->bl, AREA ); } break; case BL_MOB: { TBL_MOB *md = ((TBL_MOB*)bl); if(md->special_state.size==SZ_BIG) // tiny/big mobs [Valaris] clif_specialeffect(&md->bl,EF_GIANTBODY2,AREA); else if(md->special_state.size==SZ_MEDIUM) clif_specialeffect(&md->bl,EF_BABYBODY2,AREA); if ( md->special_state.ai == AI_ABR || md->special_state.ai == AI_BIONIC ) clif_summon_init(*md); } break; case BL_NPC: { TBL_NPC *nd = ((TBL_NPC*)bl); if( nd->size == SZ_BIG ) clif_specialeffect(&nd->bl,EF_GIANTBODY2,AREA); else if( nd->size == SZ_MEDIUM ) clif_specialeffect(&nd->bl,EF_BABYBODY2,AREA); clif_efst_status_change_sub(bl, bl, AREA); clif_progressbar_npc_area(nd); } break; case BL_PET: if (vd->head_bottom) clif_pet_equip_area((TBL_PET*)bl); // needed to display pet equip properly break; } return 0; } /// Sends information about owned homunculus to the client . [orn] /// 022e .24B .B .W .W .W .W .W .W .W .W .W .W .W .W .W .W .W .W .L .L .W .W (ZC_PROPERTY_HOMUN) /// 09f7 .24B .B .W .W .W .W .W .W .W .W .W .W .W .W .L .L .W .W .L .L .W .W (ZC_PROPERTY_HOMUN_2) void clif_hominfo( map_session_data *sd, struct homun_data *hd, int flag ){ #if PACKETVER_MAIN_NUM >= 20101005 || PACKETVER_RE_NUM >= 20080827 || defined(PACKETVER_ZERO) nullpo_retv( sd ); nullpo_retv( hd ); struct status_data *status = &hd->battle_status; struct PACKET_ZC_PROPERTY_HOMUN p; p.packetType = HEADER_ZC_PROPERTY_HOMUN; safestrncpy( p.name, hd->homunculus.name, sizeof( p.name ) ); // Bit field, bit 0 : rename_flag (1 = already renamed), bit 1 : homunc vaporized (1 = true), bit 2 : homunc dead (1 = true) p.flags = ( !battle_config.hom_rename && hd->homunculus.rename_flag ? 0x1 : 0x0 ) | ( hd->homunculus.vaporize == HOM_ST_REST ? 0x2 : 0 ) | ( hd->homunculus.hp > 0 ? 0x4 : 0 ); p.level = hd->homunculus.level; p.hunger = hd->homunculus.hunger; p.intimacy = hd->homunculus.intimacy / 100; #if !(PACKETVER_MAIN_NUM >= 20190619 || PACKETVER_RE_NUM >= 20190605 || PACKETVER_ZERO_NUM >= 20190626) p.itemId = 0; // equip id #endif p.atk2 = cap_value( status->rhw.atk2 + status->batk, 0, INT16_MAX ); p.matk = i16min( status->matk_max, INT16_MAX ); //FIXME capping to INT16 here is too late p.hit = status->hit; if( battle_config.hom_setting&HOMSET_DISPLAY_LUK ){ p.crit = status->luk / 3 + 1; //crit is a +1 decimal value! Just display purpose.[Vicious] }else{ p.crit = status->cri / 10; } #ifdef RENEWAL p.def = status->def + status->def2; p.mdef = status->mdef + status->mdef2; #else p.def = status->def + status->vit; p.mdef = status->mdef; #endif p.flee = status->flee; p.amotion = (flag) ? 0 : status->amotion; #if PACKETVER >= 20141016 // Homunculus HP bar will screw up if the percentage calculation exceeds signed values // Tested maximum: 21474836(=INT32_MAX/100), any value above will screw up the HP bar if( status->max_hp > ( INT32_MAX / 100 ) ){ p.hp = status->hp / ( status->max_hp / 100 ); p.maxHp = 100; }else{ p.hp = status->hp; p.maxHp = status->max_hp; } #else if( status->max_hp > INT16_MAX ){ p.hp = status->hp / ( status->max_hp / 100 ); p.maxHp = 100; }else{ p.hp = status->hp; p.maxHp = status->max_hp; } #endif if( status->max_sp > INT16_MAX ){ p.sp = status->sp / ( status->max_sp / 100 ); p.maxSp = 100; }else{ p.sp = status->sp; p.maxSp = status->max_sp; } #if PACKETVER_MAIN_NUM >= 20210303 || PACKETVER_RE_NUM >= 20211103 p.exp = hd->homunculus.exp; p.expNext = hd->exp_next; #else p.exp = (uint32)hd->homunculus.exp; p.expNext = (uint32)hd->exp_next; #endif switch( hom_class2type( hd->homunculus.class_ ) ){ case HT_REG: case HT_EVO: if( hd->homunculus.level >= battle_config.hom_max_level ){ p.expNext = 0; } break; case HT_S: if( hd->homunculus.level >= battle_config.hom_S_max_level ){ p.expNext = 0; } break; } p.skillPoints = hd->homunculus.skillpts; p.range = status_get_range( &hd->bl ); clif_send( &p, sizeof( p ), &sd->bl, SELF ); #endif } /// Notification about a change in homunuculus' state. /// 0230 .B .B .L .L (ZC_CHANGESTATE_MER) /// type: /// unused /// state: /// 0 = pre-init /// 1 = intimacy /// 2 = hunger /// 3 = accessory? /// ? = ignored void clif_send_homdata( homun_data& hd, e_hom_state2 state ){ #if PACKETVER >= 20050523 if( hd.master == nullptr ){ return; } uint32 param; switch( state ){ case SP_ACK: param = 0; break; case SP_INTIMATE: // TODO: Why is something like this here? [Lemongrass] if( hd.homunculus.class_ == hd.homunculusDB->evo_class && hom_intimacy_intimacy2grade( hd.homunculus.intimacy ) >= HOMGRADE_LOYAL ){ hom_calc_skilltree( &hd ); } param = hd.homunculus.intimacy / 100; break; case SP_HUNGRY: param = hd.homunculus.hunger; break; default: return; } PACKET_ZC_CHANGESTATE_MER packet{}; packet.packetType = HEADER_ZC_CHANGESTATE_MER; packet.type = 0; packet.state = state; packet.gid = hd.bl.id; packet.data = param; clif_send( &packet, sizeof( packet ), &hd.master->bl, SELF ); #endif } void clif_homskillinfoblock( homun_data& hd ){ #if PACKETVER >= 20050530 map_session_data* sd = hd.master; if( sd == nullptr ){ return; } PACKET_ZC_HOSKILLINFO_LIST* packet = reinterpret_cast(packet_buffer); packet->packetType = HEADER_ZC_HOSKILLINFO_LIST; packet->packetLength = sizeof( *packet ); for(int i = 0, count = 0; i < MAX_HOMUNSKILL; i++) { int id = hd.homunculus.hskill[i].id; if (id != 0) { int combo = (hd.homunculus.hskill[i].flag)&SKILL_FLAG_TMP_COMBO; short idx = hom_skill_get_index(id); if (idx == -1) continue; packet->skills[count].id = id; packet->skills[count].inf = (combo) ? INF_SELF_SKILL : skill_get_inf(id); packet->skills[count].unknown = 0; packet->skills[count].level = hd.homunculus.hskill[idx].lv; packet->skills[count].sp = skill_get_sp(id,hd.homunculus.hskill[idx].lv); packet->skills[count].range = skill_get_range2(&hd.bl,id,hd.homunculus.hskill[idx].lv,false); safestrncpy(packet->skills[count].name, skill_get_name(id), NAME_LENGTH); packet->skills[count].upgradable = (hd.homunculus.level < hom_skill_get_min_level(hd.homunculus.class_, id) || hd.homunculus.hskill[idx].lv >= hom_skill_tree_get_max(id, hd.homunculus.class_)) ? 0 : 1; packet->packetLength += sizeof( packet->skills[0] ); count++; } } clif_send( packet, packet->packetLength, &sd->bl, SELF ); #endif } void clif_homskillup( homun_data& hd, uint16 skill_id ){ #if PACKETVER >= 20050531 short idx = hom_skill_get_index( skill_id ); if( idx == -1 ){ return; } map_session_data* sd = hd.master; if( sd == nullptr ){ return; } PACKET_ZC_HOSKILLINFO_UPDATE packet{}; packet.packetType = HEADER_ZC_HOSKILLINFO_UPDATE; packet.skill_id = skill_id; packet.Level = hd.homunculus.hskill[idx].lv; packet.SP = skill_get_sp(skill_id,hd.homunculus.hskill[idx].lv); packet.AttackRange = skill_get_range2(&hd.bl,skill_id,hd.homunculus.hskill[idx].lv,false); packet.upgradable = (hd.homunculus.level < hom_skill_get_min_level(hd.homunculus.class_, skill_id) || hd.homunculus.hskill[idx].lv >= hom_skill_tree_get_max(skill_id, hd.homunculus.class_)) ? 0 : 1; clif_send( &packet, sizeof( packet ), &sd->bl, SELF ); #endif } /// Result of request to feed a homun/merc. /// 022f .B .W (ZC_FEED_MER) /// result: /// 0 = failure /// 1 = success void clif_hom_food( map_session_data& sd, int32 foodid, bool success ){ PACKET_ZC_FEED_MER packet{}; packet.packetType = HEADER_ZC_FEED_MER; packet.result = success; packet.itemId = client_nameid( foodid ); clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Notifies the client, that it is walking. /// 0087 .L .6B (ZC_NOTIFY_PLAYERMOVE) void clif_walkok( map_session_data& sd ){ PACKET_ZC_NOTIFY_PLAYERMOVE packet{}; packet.packetType = HEADER_ZC_NOTIFY_PLAYERMOVE; packet.moveStartTime = client_tick(gettick()); WBUFPOS2(packet.moveData, 0, sd.bl.x, sd.bl.y, sd.ud.to_x, sd.ud.to_y, 8, 8); clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Notifies clients in an area, that an other visible object is walking. /// Note: unit must not be self void clif_move( struct unit_data& ud ) { struct block_list* bl = ud.bl; struct view_data* vd = status_get_viewdata(bl); if (bl == nullptr || vd == nullptr) return; // This performance check is needed to keep GM-hidden objects from being notified to bots. if (vd->class_ == JT_INVISIBLE) return; // Hide NPC from Maya Purple card if (clif_npc_mayapurple(bl)) return; status_change* sc = nullptr; if ((sc = status_get_sc(bl)) && sc->option & (OPTION_HIDE | OPTION_CLOAK | OPTION_INVISIBLE | OPTION_CHASEWALK)) clif_ally_only = true; clif_set_unit_walking( *bl, nullptr, ud, AREA_WOS ); if (vd->cloth_color) clif_refreshlook(bl, bl->id, LOOK_CLOTHES_COLOR, vd->cloth_color, AREA_WOS); if (vd->body_style) clif_refreshlook(bl, bl->id, LOOK_BODY2, vd->body_style, AREA_WOS); switch (bl->type) { case BL_PC: { map_session_data* sd = reinterpret_cast( bl ); if (sd->state.size == SZ_BIG) // tiny/big players [Valaris] clif_specialeffect(&sd->bl, EF_GIANTBODY2, AREA); else if (sd->state.size == SZ_MEDIUM) clif_specialeffect(&sd->bl, EF_BABYBODY2, AREA); if (sd->status.robe) clif_refreshlook(bl, bl->id, LOOK_ROBE, sd->status.robe, AREA); } break; case BL_MOB: { mob_data* md = reinterpret_cast( bl ); if (md->special_state.size == SZ_BIG) // tiny/big mobs [Valaris] clif_specialeffect(&md->bl, EF_GIANTBODY2, AREA); else if (md->special_state.size == SZ_MEDIUM) clif_specialeffect(&md->bl, EF_BABYBODY2, AREA); } break; case BL_PET: if (vd->head_bottom) // needed to display pet equip properly clif_pet_equip_area(BL_CAST(BL_PET, bl)); break; } clif_ally_only = false; } /*========================================== * Delays the map_quit of a player after they are disconnected. [Skotlex] *------------------------------------------*/ static TIMER_FUNC(clif_delayquit){ map_session_data *sd = nullptr; //Remove player from map server if ((sd = map_id2sd(id)) != nullptr && sd->fd == 0) //Should be a disconnected player. map_quit(sd); return 0; } /*========================================== * *------------------------------------------*/ void clif_quitsave(int fd,map_session_data *sd) { if (!battle_config.prevent_logout || sd->canlog_tick == 0 || DIFF_TICK(gettick(), sd->canlog_tick) > battle_config.prevent_logout) map_quit(sd); else if (session_isValid(sd->fd)) { //Disassociate session from player (session is deleted after this function was called) //And set a timer to make him quit later. session[sd->fd]->session_data = nullptr; sd->fd = 0; add_timer(gettick() + 10000, clif_delayquit, sd->bl.id, 0); } } /// Notifies the client of a position change to coordinates on given map (ZC_NPCACK_MAPMOVE). /// 0091 .16B .W .W void clif_changemap( map_session_data& sd, short m, uint16 x, uint16 y ){ PACKET_ZC_NPCACK_MAPMOVE packet{}; packet.packetType = HEADER_ZC_NPCACK_MAPMOVE; mapindex_getmapname_ext(map_mapid2mapname(m), packet.mapName); packet.xPos = x; packet.yPos = y; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Notifies the client of a position change to coordinates on given map, which is on another map-server. /// 0092 .16B .W .W .L .W (ZC_NPCACK_SERVERMOVE) /// 0ac7 .16B .W .W .L .W .128B (ZC_NPCACK_SERVERMOVE_DOMAIN) void clif_changemapserver( map_session_data& sd, const char* map, uint16 x, uint16 y, uint32 ip, uint16 port ){ PACKET_ZC_NPCACK_SERVERMOVE packet{}; packet.packetType = HEADER_ZC_NPCACK_SERVERMOVE; mapindex_getmapname_ext(map, packet.mapName); packet.xPos = x; packet.yPos = y; packet.ip = htonl(ip); packet.port = ntows(htons(port)); // [!] LE byte order here [!] #if PACKETVER >= 20170315 safestrncpy( packet.domain, "", sizeof( packet.domain ) ); #endif #ifdef DEBUG ShowDebug( "Sending the client (%d %d.%d.%d.%d) to map-server with ip %d.%d.%d.%d and port %hu\n", sd.status.account_id, CONVIP(session[sd.fd]->client_addr), CONVIP(ip), port ); #endif clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// In many situations (knockback, backslide, etc.) Aegis sends both clif_slide and clif_fixpos /// This function combines both calls and allows to simplify the calling code void clif_blown(struct block_list *bl) { clif_slide(bl, bl->x, bl->y); clif_fixpos( *bl ); } /// Visually moves(slides) a character to x,y. If the target cell /// isn't walkable, the char doesn't move at all. If the char is /// sitting it will stand up. /// 0088 .L .W .W (ZC_STOPMOVE) void clif_fixpos( block_list& bl ){ PACKET_ZC_STOPMOVE packet = {}; packet.packetType = HEADER_ZC_STOPMOVE; packet.AID = bl.id; packet.xPos = bl.x; packet.yPos = bl.y; clif_send( &packet, sizeof( packet ), &bl, AREA); if( disguised( &bl ) ){ packet.AID = disguised_bl_id( bl.id ); clif_send( &packet, sizeof( packet ), &bl, SELF ); } } /// Displays the buy/sell dialog of an NPC shop. /// 00c4 .L (ZC_SELECT_DEALTYPE) void clif_npcbuysell( map_session_data& sd, npc_data& nd ){ PACKET_ZC_SELECT_DEALTYPE packet{}; packet.packetType = HEADER_ZC_SELECT_DEALTYPE; packet.npcId = nd.bl.id; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Presents list of items, that can be bought in an NPC shop. /// 00c6 .W { .L .L .B .W }* (ZC_PC_PURCHASE_ITEMLIST) void clif_buylist( map_session_data& sd, npc_data& nd ){ PACKET_ZC_PC_PURCHASE_ITEMLIST* p = reinterpret_cast( packet_buffer ); p->packetType = HEADER_ZC_PC_PURCHASE_ITEMLIST; p->packetLength = sizeof( *p ); for( int i = 0, count = 0, discount = npc_shop_discount( &nd ); i < nd.u.shop.count; i++ ){ int val = nd.u.shop.shop_item[i].value; p->items[count].price = val; p->items[count].discountPrice = ( discount ) ? pc_modifybuyvalue( &sd, val ) : val; p->items[count].itemType = itemtype( nd.u.shop.shop_item[i].nameid ); p->items[count].itemId = client_nameid( nd.u.shop.shop_item[i].nameid ); #if PACKETVER_MAIN_NUM >= 20210203 || PACKETVER_RE_NUM >= 20211103 std::shared_ptr id = item_db.find(nd.u.shop.shop_item[i].nameid); p->items[count].viewSprite = id->look; p->items[count].location = pc_equippoint_sub( &sd, id.get() ); #endif p->packetLength += static_castpacketLength)>( sizeof( p->items[0] ) ); count++; } clif_send( p, p->packetLength, &sd.bl, SELF ); } /// Presents list of items, that can be sold to an NPC shop. /// 00c7 .W { .W .L .L }* (ZC_PC_SELL_ITEMLIST) void clif_selllist( map_session_data& sd){ if( !sd.npc_shopid ){ return; } npc_data* nd = map_id2nd( sd.npc_shopid ); if( nd == nullptr ){ return; } PACKET_ZC_PC_SELL_ITEMLIST *packet = reinterpret_cast(packet_buffer); packet->packetType = HEADER_ZC_PC_SELL_ITEMLIST; packet->packetLength = sizeof( *packet ); for( int i = 0, c = 0; i < MAX_INVENTORY; i++ ){ if( sd.inventory.u.items_inventory[i].nameid <= 0 ){ continue; } if( sd.inventory_data[i] == nullptr ){ continue; } if( !pc_can_sell_item( &sd, &sd.inventory.u.items_inventory[i], nd->subtype ) ){ continue; } int price; if( battle_config.rental_item_novalue && sd.inventory.u.items_inventory[i].expire_time ){ price = 0; }else{ price = sd.inventory_data[i]->value_sell; if( price < 0 ){ continue; } } packet->items[c].index = client_index(i); packet->items[c].price = price; packet->items[c].overcharge = pc_modifysellvalue( &sd, price ); packet->packetLength += sizeof( packet->items[0] ); c++; } clif_send( packet, packet->packetLength, &sd.bl, SELF ); } /// Closes shop (CZ_NPC_TRADE_QUIT). /// 09d4 void clif_parse_NPCShopClosed(int fd, map_session_data *sd) { // TODO: State tracking? sd->npc_shopid = 0; } /** * Presents list of items, that can be sold to a Market shop. * @author: Ind and Yommy **/ /// 0x9d5 .W { .W .B .L .L .W }* (ZC_NPC_MARKET_OPEN) void clif_npc_market_open(map_session_data *sd, struct npc_data *nd) { #if PACKETVER >= 20131223 nullpo_retv( sd ); nullpo_retv( nd ); if( sd->state.trading ){ return; } int fd = sd->fd; WFIFOHEAD( fd, sizeof( struct PACKET_ZC_NPC_MARKET_OPEN ) + nd->u.shop.count * sizeof( struct PACKET_ZC_NPC_MARKET_OPEN_sub ) ); struct PACKET_ZC_NPC_MARKET_OPEN *p = (struct PACKET_ZC_NPC_MARKET_OPEN *)WFIFOP( fd, 0 ); p->packetType = HEADER_ZC_NPC_MARKET_OPEN; int count = 0; for( int i = 0; i < nd->u.shop.count; i++ ){ struct npc_item_list *item = &nd->u.shop.shop_item[i]; if( !item->nameid ){ continue; } std::shared_ptr id = item_db.find(item->nameid); if( !id ){ continue; } p->list[count].nameid = client_nameid( item->nameid ); p->list[count].type = itemtype( item->nameid ); p->list[count].price = item->value; p->list[count].qty = item->qty; p->list[count].weight = id->weight; #if PACKETVER_MAIN_NUM >= 20210203 || PACKETVER_RE_NUM >= 20211103 p->list[count].location = pc_equippoint_sub( sd, id.get() ); #endif count++; } p->packetLength = static_castpacketLength)>( sizeof( struct PACKET_ZC_NPC_MARKET_OPEN ) + count * sizeof( struct PACKET_ZC_NPC_MARKET_OPEN_sub ) ); WFIFOSET( fd, p->packetLength ); sd->state.trading = 1; #endif } /// Closes the Market shop window. void clif_parse_NPCMarketClosed(int fd, map_session_data *sd) { nullpo_retv(sd); sd->npc_shopid = 0; sd->state.trading = 0; } /// Purchase item from Market shop. /// 0x9d7 .W .B { .W .W .L }* (ZC_NPC_MARKET_PURCHASE_RESULT) void clif_npc_market_purchase_ack( map_session_data& sd, e_purchase_result res, std::vector& list ){ #if PACKETVER >= 20131223 struct npc_data *nd = map_id2nd( sd.npc_shopid ); if( nd == nullptr ){ return; } PACKET_ZC_NPC_MARKET_PURCHASE_RESULT* p = reinterpret_cast( packet_buffer ); p->PacketType = HEADER_ZC_NPC_MARKET_PURCHASE_RESULT; p->PacketLength = sizeof( struct PACKET_ZC_NPC_MARKET_PURCHASE_RESULT ); #if PACKETVER_MAIN_NUM >= 20190807 || PACKETVER_RE_NUM >= 20190807 || PACKETVER_ZERO_NUM >= 20190814 p->result = ( res == e_purchase_result::PURCHASE_SUCCEED ? 0 : -1 ); #else p->result = ( res == e_purchase_result::PURCHASE_SUCCEED ? 1 : 0 ); #endif if( res == e_purchase_result::PURCHASE_SUCCEED ){ for( int i = 0, j, count = 0; i < list.size(); i++ ){ ARR_FIND( 0, nd->u.shop.count, j, list[i].nameid == nd->u.shop.shop_item[j].nameid ); // Not found if( j == nd->u.shop.count ){ continue; } p->list[count].ITID = client_nameid( list[i].nameid ); p->list[count].qty = list[i].qty; p->list[count].price = nd->u.shop.shop_item[j].value; p->PacketLength += static_castPacketLength)>( sizeof( p->list[0] ) ); count++; } } clif_send( p, p->PacketLength, &sd.bl, SELF ); #endif } /// Purchase item from Market shop. /// 0x9d6 .W { .W .L } (CZ_NPC_MARKET_PURCHASE) void clif_parse_NPCMarketPurchase(int fd, map_session_data *sd) { #if PACKETVER >= 20131223 nullpo_retv( sd ); if( !sd->npc_shopid ){ return; } const struct PACKET_CZ_NPC_MARKET_PURCHASE* p = (struct PACKET_CZ_NPC_MARKET_PURCHASE*)RFIFOP( fd, 0 ); int count = ( p->PacketLength - sizeof( struct PACKET_CZ_NPC_MARKET_PURCHASE ) ) / sizeof( struct PACKET_CZ_NPC_MARKET_PURCHASE_sub ); std::vector items; items.reserve( count ); for( int i = 0; i < count; i++ ){ s_npc_buy_list item = {}; item.nameid = p->list[i].ITID; item.qty = p->list[i].qty; items.push_back( item ); } e_purchase_result res = npc_buylist( sd, items ); clif_npc_market_purchase_ack( *sd, res, items ); #endif } /// Displays an NPC dialog message. /// 00b4 .W .L .?B (ZC_SAY_DIALOG) /// Client behavior (dialog window): /// - disable mouse targeting /// - open the dialog window /// - set npcid of dialog window (0 by default) /// - if set to clear on next mes, clear contents /// - append this text void clif_scriptmes( map_session_data& sd, uint32 npcid, const char *mes ){ PACKET_ZC_SAY_DIALOG* p = reinterpret_cast( packet_buffer ); int16 length = (int16)( strlen( mes ) + 1 ); p->PacketType = HEADER_ZC_SAY_DIALOG; p->PacketLength = sizeof( *p ) + length; p->NpcID = npcid; safestrncpy( p->message, mes, length ); clif_send( p, p->PacketLength, &sd.bl, SELF ); } /// Adds a 'next' button to an NPC dialog. /// 00b5 .L (ZC_WAIT_DIALOG) /// Client behavior (dialog window): /// - disable mouse targeting /// - open the dialog window /// - add 'next' button /// When 'next' is pressed: /// - 00B9 .L /// - set to clear on next mes /// - remove 'next' button void clif_scriptnext( map_session_data& sd, uint32 npcid ){ struct PACKET_ZC_WAIT_DIALOG p = {}; p.PacketType = HEADER_ZC_WAIT_DIALOG; p.NpcID = npcid; clif_send( &p, sizeof( p ), &sd.bl, SELF ); } /// Adds a 'close' button to an NPC dialog. /// 00b6 .L (ZC_CLOSE_DIALOG) /// Client behavior: /// - if dialog window is open: /// - remove 'next' button /// - add 'close' button /// - else: /// - enable mouse targeting /// - close the dialog window /// - close the menu window /// When 'close' is pressed: /// - enable mouse targeting /// - close the dialog window /// - close the menu window /// - 0146 .L void clif_scriptclose( map_session_data& sd, uint32 npcid ){ PACKET_ZC_CLOSE_DIALOG packet{}; packet.packetType = HEADER_ZC_CLOSE_DIALOG; packet.npcId = npcid; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /** * Close script when player is idle * 08d6 .L (ZC_CLEAR_DIALOG) * @author [Ind/Hercules] * @param sd : player pointer * @param npcid : npc gid to close */ void clif_scriptclear( map_session_data& sd, int npcid ){ struct PACKET_ZC_CLEAR_DIALOG p = {}; p.packetType = HEADER_ZC_CLEAR_DIALOG; p.GID = npcid; clif_send( &p, sizeof( p ), &sd.bl, SELF ); } void clif_sendfakenpc( map_session_data& sd, uint32 npcid ){ sd.state.using_fake_npc = 1; // Code below is a copy of clif_set_unit_idle struct packet_idle_unit p; p.PacketType = idle_unitType; #if PACKETVER >= 20091103 p.PacketLength = sizeof(p); p.objecttype = 0x1; // NPC_TYPE #endif #if PACKETVER >= 20131223 p.AID = npcid; p.GID = 0; #else p.GID = npcid; #endif p.speed = 0; p.bodyState = 0; p.healthState = 0; p.effectState = 0; p.job = JT_HIDDEN_NPC; p.head = 0; p.weapon = 0; #if PACKETVER < 7 || PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 p.shield = 0; #endif p.accessory = 0; p.accessory2 = 0; p.accessory3 = 0; p.headpalette = 0; p.bodypalette = 0; p.headDir = 0; #if PACKETVER >= 20101124 p.robe = 0; #endif p.GUID = 0; p.GEmblemVer = 0; p.honor = 0; p.virtue = 0; p.isPKModeON = 0; p.sex = SEX_FEMALE; WBUFPOS( &p.PosDir[0], 0, sd.bl.x, sd.bl.y, sd.ud.dir ); p.xSize = p.ySize = 5; p.state = 0; p.clevel = 0; #if PACKETVER >= 20080102 p.font = 0; #endif #if PACKETVER >= 20120221 p.maxHP = -1; p.HP = -1; #endif #if PACKETVER >= 20150513 p.body = 0; #endif /* Might be earlier, this is when the named item bug began */ #if PACKETVER >= 20131223 safestrncpy( p.name, "", NAME_LENGTH); #endif clif_send( &p, sizeof( p ), &sd.bl, SELF ); } /// Displays an NPC dialog menu. /// 00b7 .W .L .?B (ZC_MENU_LIST) /// Client behavior: /// - disable mouse targeting /// - close the menu window /// - open the menu window /// - add options to the menu (separated in the text by ":") /// - set npcid of menu window /// - if dialog window is open: /// - remove 'next' button /// When 'ok' is pressed: /// - 00B8 .L .B /// - close the menu window /// When 'cancel' is pressed: /// - 00B8 .L <-1>.B /// - enable mouse targeting /// - close a bunch of windows... /// WARNING: the 'cancel' button closes other windows besides the dialog window and the menu window. /// Which suggests their have intertwined behavior. (probably the mouse targeting) /// TODO investigate behavior of other windows [FlavioJS] void clif_scriptmenu( map_session_data& sd, uint32 npcid, const char* mes ){ struct block_list *bl = nullptr; if (!sd.state.using_fake_npc && (npcid == fake_nd->bl.id || ((bl = map_id2bl(npcid)) && (bl->m!=sd.bl.m || bl->xx>sd.bl.x+AREA_SIZE+1 || bl->yy>sd.bl.y+AREA_SIZE+1)))) clif_sendfakenpc( sd, npcid ); // String length + 1 byte for zero termination size_t mes_length = strlen( mes ) + 1; PACKET_ZC_MENU_LIST* packet = reinterpret_cast( packet_buffer ); packet->packetType = HEADER_ZC_MENU_LIST; packet->packetLength = sizeof( PACKET_ZC_MENU_LIST ); packet->npcId = npcid; safestrncpy( packet->menu, mes, mes_length ); packet->packetLength += static_castpacketLength)>( mes_length ); clif_send( packet, packet->packetLength, &sd.bl, SELF ); } /// Displays an NPC dialog input box for numbers. /// 0142 .L (ZC_OPEN_EDITDLG) /// Client behavior (inputnum window): /// - if npcid exists in the client: /// - open the inputnum window /// - set npcid of inputnum window /// When 'ok' is pressed: /// - if inputnum window has text: /// - if npcid exists in the client: /// - 0143 .L .L /// - close inputnum window void clif_scriptinput( map_session_data& sd, uint32 npcid ){ struct block_list *bl = nullptr; if (!sd.state.using_fake_npc && (npcid == fake_nd->bl.id || ((bl = map_id2bl(npcid)) && (bl->m!=sd.bl.m || bl->xx>sd.bl.x+AREA_SIZE+1 || bl->yy>sd.bl.y+AREA_SIZE+1)))) clif_sendfakenpc( sd, npcid ); PACKET_ZC_OPEN_EDITDLG packet{}; packet.packetType = HEADER_ZC_OPEN_EDITDLG; packet.npcId = npcid; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Displays an NPC dialog input box for strings. /// 01d4 .L (ZC_OPEN_EDITDLGSTR) /// Client behavior (inputstr window): /// - if npcid is 0 or npcid exists in the client: /// - open the inputstr window /// - set npcid of inputstr window /// When 'ok' is pressed: /// - if inputstr window has text and isn't an insult(manner.txt): /// - if npcid is 0 or npcid exists in the client: /// - 01d5 .W .L .?B /// - close inputstr window void clif_scriptinputstr( map_session_data& sd, uint32 npcid ){ struct block_list *bl = nullptr; if (!sd.state.using_fake_npc && (npcid == fake_nd->bl.id || ((bl = map_id2bl(npcid)) && (bl->m!=sd.bl.m || bl->xx>sd.bl.x+AREA_SIZE+1 || bl->yy>sd.bl.y+AREA_SIZE+1)))) clif_sendfakenpc( sd, npcid ); PACKET_ZC_OPEN_EDITDLGSTR packet{}; packet.packetType = HEADER_ZC_OPEN_EDITDLGSTR; packet.npcId = npcid; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Marks a position on client's minimap. /// 0144 .L .L .L .L .B .L (ZC_COMPASS) /// npc id: /// is ignored in the client /// type: /// 0 = display mark for 15 seconds /// 1 = display mark until dead or teleported /// 2 = remove mark /// color: /// 0x00RRGGBB void clif_viewpoint( map_session_data& sd, uint32 npc_id, int type, uint16 x, uint16 y, int id, uint32 color ){ PACKET_ZC_COMPASS packet{}; packet.packetType = HEADER_ZC_COMPASS; packet.npcId = npc_id; packet.type = type; packet.xPos = x; packet.yPos = y; packet.id = id; packet.color = color; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /// Displays an illustration image. /// 0145 .16B .B (ZC_SHOW_IMAGE) /// 01b3 .64B .B (ZC_SHOW_IMAGE2) /// type: /// 0 = bottom left corner /// 1 = bottom middle /// 2 = bottom right corner /// 3 = middle of screen, inside a movable window /// 4 = middle of screen, movable with a close button, chrome-less /// 255 = clear all displayed cutins void clif_cutin( map_session_data& sd, const char* image, int type ){ PACKET_ZC_SHOW_IMAGE packet{}; packet.packetType = HEADER_ZC_SHOW_IMAGE; safestrncpy(packet.image, image, sizeof(packet.image)); packet.type = type; clif_send( &packet, sizeof( packet ), &sd.bl, SELF ); } /*========================================== * Fills in card data from the given item and into the buffer. [Skotlex] *------------------------------------------*/ static void clif_addcards( struct EQUIPSLOTINFO* buf, struct item* item ){ nullpo_retv( buf ); // Blank data if( item == nullptr ){ buf->card[0] = 0; buf->card[1] = 0; buf->card[2] = 0; buf->card[3] = 0; return; } // Pet eggs if( item->card[0] == CARD0_PET ){ buf->card[0] = 0; buf->card[1] = 0; // Pet intimacy // No idea when this was added exactly, but older clients have no problem if we send it anyway buf->card[2] = item->card[3] >> 1; // Pet renamed flag buf->card[3] = item->card[3] & 1; return; } // Forged/created items if( item->card[0] == CARD0_FORGE || item->card[0] == CARD0_CREATE ){ buf->card[0] = item->card[0]; buf->card[1] = item->card[1]; buf->card[2] = item->card[2]; buf->card[3] = item->card[3]; return; } int i = 0, j; // Client only receives four cards.. so randomly send them a set of cards. [Skotlex] if( MAX_SLOTS > 4 && ( j = itemdb_slots( item->nameid ) ) > 4 ){ i = rnd() % ( j - 3 ); //eg: 6 slots, possible i values: 0->3, 1->4, 2->5 => i = rnd()%3; } // Normal items. for( int k = 0; k < 4; k++, i++ ){ if( item->card[i] > 0 ){ buf->card[k] = client_nameid( item->card[i] ); }else{ buf->card[k] = 0; } } } /// Fills in part of the item buffers that calls for variable bonuses data. [Napster] /// A maximum of 5 random options can be supported. static void clif_add_random_options( struct ItemOptions buf[MAX_ITEM_RDM_OPT], struct item& it ){ for( int i = 0; i < MAX_ITEM_RDM_OPT; i++ ){ if( it.option[i].id ){ buf[i].index = it.option[i].id; // OptIndex buf[i].value = it.option[i].value; // Value buf[i].param = it.option[i].param; // Param1 }else{ buf[i].index = 0; buf[i].value = 0; buf[i].param = 0; } } #if MAX_ITEM_RDM_OPT < 5 for( ; i < 5; i++ ){ buf[i].index = 0; // OptIndex buf[i].value = 0; // Value buf[i].param = 0; // Param1 } #endif } /// Notifies the client, about a received inventory item or the result of a pick-up request. /// 00a0 .W .W .W .B .B .B .W .W .W .W .W .B .B (ZC_ITEM_PICKUP_ACK) /// 029a .W .W .W .B .B .B .W .W .W .W .W .B .B .L (ZC_ITEM_PICKUP_ACK2) /// 02d4 .W .W .W .B .B .B .W .W .W .W .W .B .B .L .W (ZC_ITEM_PICKUP_ACK3) /// 0990 .W .W .W .B .B .B .W .W .W .W .L .B .B .L .W (ZC_ITEM_PICKUP_ACK_V5) /// 0a0c .W .W .W .B .B .B .W .W .W .W .L .B .B .L .W {