diff --git a/conf/battle/client.conf b/conf/battle/client.conf index cc1035a1e4..4cf35217dc 100644 --- a/conf/battle/client.conf +++ b/conf/battle/client.conf @@ -15,11 +15,11 @@ min_chat_delay: 0 // Valid range of dyes and styles on the client. min_hair_style: 0 -max_hair_style: 27 +max_hair_style: 42 min_hair_color: 0 max_hair_color: 8 min_cloth_color: 0 -max_cloth_color: 4 +max_cloth_color: 7 min_body_style: 0 max_body_style: 1 diff --git a/conf/msg_conf/map_msg.conf b/conf/msg_conf/map_msg.conf index b5ce3a6d3e..59b211d65f 100644 --- a/conf/msg_conf/map_msg.conf +++ b/conf/msg_conf/map_msg.conf @@ -876,7 +876,9 @@ 797: This command is unavailable to non-4th class. -//798-799 free +// @stylist +798: This command requires packet version 2015-11-04 or newer. +799: You have already opened the stylist UI. 800: Dragon Knight 801: Meister diff --git a/db/import-tmpl/stylist.yml b/db/import-tmpl/stylist.yml new file mode 100644 index 0000000000..fc94888688 --- /dev/null +++ b/db/import-tmpl/stylist.yml @@ -0,0 +1,41 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +########################################################################### +# Stylist Database +########################################################################### +# +# Stylist Settings +# +########################################################################### +# - Look Look that will be changed. +# Options Possible options to select from. +# - Index Client side index of the option. +# Value Value of the look (can also be an item name). +# CostsHuman: Costs for human players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +# CostsDoram: Costs for doram players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +########################################################################### + +Header: + Type: STYLIST_DB + Version: 1 diff --git a/db/re/item_db_etc.yml b/db/re/item_db_etc.yml index f82a59c873..b4013b48bc 100644 --- a/db/re/item_db_etc.yml +++ b/db/re/item_db_etc.yml @@ -21686,8 +21686,8 @@ Body: NoMail: true NoAuction: true - Id: 6707 - AegisName: Jeremy_Beauty_Coupon - Name: Jeremy Beauty Coupon + AegisName: J_Shop_Coupon + Name: Cash Hair Coupon Type: Etc Buy: 10 Weight: 10 @@ -24023,9 +24023,10 @@ Body: Type: Etc Buy: 10 - Id: 6959 - AegisName: aegis_6959 - Name: Costume Change Ticket + AegisName: Costume_Ticket + Name: Costume Change Ticket Type: Etc + Buy: 0 Trade: Override: 100 NoDrop: true diff --git a/db/re/item_db_usable.yml b/db/re/item_db_usable.yml index f6b0596df6..527a7efd08 100644 --- a/db/re/item_db_usable.yml +++ b/db/re/item_db_usable.yml @@ -39884,6 +39884,57 @@ Body: NoGuildStorage: true NoMail: true NoAuction: true + - Id: 16843 + AegisName: C_New_Style_Box + Name: Beauty Gift Box + Type: Cash + Buy: 20 + Weight: 10 + Trade: + Override: 100 + NoDrop: true + NoTrade: true + NoSell: true + NoCart: true + NoGuildStorage: true + NoMail: true + NoAuction: true + Script: | + getitem 7622,1; + - Id: 16854 + AegisName: CCloth_Dye_Coupon_Box + Name: Clothing Dye Box + Type: Cash + Buy: 20 + Weight: 10 + Trade: + Override: 100 + NoDrop: true + NoTrade: true + NoSell: true + NoCart: true + NoGuildStorage: true + NoMail: true + NoAuction: true + Script: | + getitem 6046,1; + - Id: 16855 + AegisName: CCloth_Dye_Coupon2_Box + Name: Clothing Dye Orig Box + Type: Cash + Buy: 20 + Weight: 10 + Trade: + Override: 100 + NoDrop: true + NoTrade: true + NoSell: true + NoCart: true + NoGuildStorage: true + NoMail: true + NoAuction: true + Script: | + getitem 6047,1; - Id: 16864 AegisName: Siege_Map_Teleport_Scroll_Box_10 Name: Siege Map Teleport Scroll Box(10) @@ -42918,6 +42969,23 @@ Body: NoAuction: true Script: | getgroupitem(IG_Event_Almighty_Box_100); + - Id: 17336 + AegisName: J_Shop_Coupon_Box + Name: Cash Hair Coupon Box + Type: Cash + Buy: 20 + Weight: 10 + Trade: + Override: 100 + NoDrop: true + NoTrade: true + NoSell: true + NoCart: true + NoGuildStorage: true + NoMail: true + NoAuction: true + Script: | + getitem 6707,1; - Id: 17337 AegisName: Holy_Spirit_Scroll Name: Holy Spirit Egg diff --git a/db/re/stylist.yml b/db/re/stylist.yml new file mode 100644 index 0000000000..9e4be0a8bc --- /dev/null +++ b/db/re/stylist.yml @@ -0,0 +1,429 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +########################################################################### +# Stylist Database +########################################################################### +# +# Stylist Settings +# +########################################################################### +# - Look Look that will be changed. +# Options Possible options to select from. +# - Index Client side index of the option. +# Value Value of the look (can also be an item name). +# CostsHuman: Costs for human players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +# CostsDoram: Costs for doram players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +########################################################################### + +Header: + Type: STYLIST_DB + Version: 1 + +Body: + - Look: Hair_Color + Options: + - Index: -1 + Value: 0 + CostsHuman: + Price: 0 + CostsDoram: + Price: 0 + - Index: 1 + Value: 1 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 2 + Value: 2 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 3 + Value: 3 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 4 + Value: 4 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 5 + Value: 5 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 6 + Value: 6 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 7 + Value: 7 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 8 + Value: 8 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Look: Hair + Options: + - Index: 1 + Value: 1 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 2 + Value: 2 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 3 + Value: 3 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 4 + Value: 4 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 5 + Value: 5 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 6 + Value: 6 + CostsHuman: + Price: 100000 + CostsDoram: + Price: 100000 + - Index: 7 + Value: 7 + CostsHuman: + Price: 100000 + CostDoram: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 8 + Value: 8 + CostsHuman: + Price: 100000 + CostDoram: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 9 + Value: 9 + CostsHuman: + Price: 100000 + CostDoram: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 10 + Value: 10 + CostsHuman: + Price: 100000 + CostDoram: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 11 + Value: 11 + CostsHuman: + Price: 100000 + - Index: 12 + Value: 12 + CostsHuman: + Price: 100000 + - Index: 13 + Value: 13 + CostsHuman: + Price: 100000 + - Index: 14 + Value: 14 + CostsHuman: + Price: 100000 + - Index: 15 + Value: 15 + CostsHuman: + Price: 100000 + - Index: 16 + Value: 16 + CostsHuman: + Price: 100000 + - Index: 17 + Value: 17 + CostsHuman: + Price: 100000 + - Index: 18 + Value: 18 + CostsHuman: + Price: 100000 + - Index: 19 + Value: 19 + CostsHuman: + Price: 100000 + - Index: 20 + Value: 20 + CostsHuman: + Price: 100000 + - Index: 21 + Value: 21 + CostsHuman: + Price: 100000 + - Index: 22 + Value: 22 + CostsHuman: + Price: 100000 + - Index: 23 + Value: 23 + CostsHuman: + Price: 100000 + - Index: 24 + Value: 24 + CostsHuman: + RequiredItem: New_Style_Coupon + RequiredItemBox: C_New_Style_Box + - Index: 25 + Value: 25 + CostsHuman: + RequiredItem: New_Style_Coupon + RequiredItemBox: C_New_Style_Box + - Index: 26 + Value: 26 + CostsHuman: + RequiredItem: New_Style_Coupon + RequiredItemBox: C_New_Style_Box + - Index: 27 + Value: 27 + CostsHuman: + RequiredItem: New_Style_Coupon + RequiredItemBox: C_New_Style_Box + - Index: 28 + Value: 28 + CostsHuman: + RequiredItem: J_Shop_Coupon + RequiredItemBox: J_Shop_Coupon_Box + - Index: 29 + Value: 29 + CostsHuman: + RequiredItem: J_Shop_Coupon + RequiredItemBox: J_Shop_Coupon_Box + - Index: 30 + Value: 30 + CostsHuman: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 31 + Value: 31 + CostsHuman: + RequiredItem: J_Shop_Coupon2 + RequiredItemBox: J_Shop_Coupon2 + - Index: 32 + Value: 32 + CostsHuman: + Price: 100000 + - Index: 33 + Value: 33 + CostsHuman: + Price: 3000000 + - Index: 34 + Value: 34 + CostsHuman: + Price: 3000000 + - Index: 35 + Value: 35 + CostsHuman: + Price: 3000000 + - Index: 36 + Value: 36 + CostsHuman: + Price: 3000000 + - Index: 37 + Value: 37 + CostsHuman: + Price: 3000000 + - Index: 38 + Value: 38 + CostsHuman: + Price: 3000000 + - Index: 39 + Value: 39 + CostsHuman: + Price: 3000000 + - Index: 40 + Value: 40 + CostsHuman: + Price: 3000000 + - Index: 41 + Value: 41 + CostsHuman: + Price: 3000000 + - Index: 42 + Value: 42 + CostsHuman: + Price: 3000000 + - Look: Clothes_Color + Options: + - Index: 1 + Value: 0 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon_II + RequiredItemBox: CCloth_Dye_Coupon2_Box + CostsDoram: + RequiredItem: Clothing_Dye_Coupon_II + RequiredItemBox: CCloth_Dye_Coupon2_Box + - Index: 2 + Value: 2 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + CostsDoram: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Index: 3 + Value: 3 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + CostsDoram: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Index: 4 + Value: 4 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Index: 5 + Value: 5 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Index: 6 + Value: 6 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Index: 7 + Value: 7 + CostsHuman: + RequiredItem: Clothing_Dye_Coupon + RequiredItemBox: CCloth_Dye_Coupon_Box + - Look: Head_Top + Options: + - Index: 1 + Value: Hat + CostsHuman: + Price: 1000 + CostsDoram: + Price: 1000 + - Index: 2 + Value: Ribbon + CostsHuman: + Price: 800 + CostsDoram: + Price: 800 + - Index: 3 + Value: Bandana + CostsHuman: + Price: 400 + CostsDoram: + Price: 400 + - Look: Head_Mid + Options: + - Index: 1 + Value: One_Eyed_Glass + CostsHuman: + Price: 10000 + CostsDoram: + Price: 10000 + - Index: 2 + Value: Sunglasses + CostsHuman: + Price: 5000 + CostsDoram: + Price: 5000 + - Index: 3 + Value: Luxury_Sunglasses + CostsHuman: + Price: 24000 + CostsDoram: + Price: 24000 + - Index: 4 + Value: Spinning_Eyes + CostsHuman: + Price: 20000 + CostsDoram: + Price: 20000 + - Index: 5 + Value: Diver's_Goggles + CostsHuman: + Price: 3500 + CostsDoram: + Price: 3500 + - Index: 6 + Value: Glasses + CostsHuman: + Price: 4000 + CostsDoram: + Price: 4000 + - Index: 7 + Value: Eye_Bandage + CostsHuman: + Price: 1000 + CostsDoram: + Price: 1000 + - Look: Head_Bottom + Options: + - Index: 1 + Value: Granpa_Beard + CostsHuman: + Price: 5000 + CostsDoram: + Price: 5000 + - Look: Body2 + Options: + - Index: 1 + Value: 0 + CostsHuman: + RequiredItem: Costume_Ticket + - Index: 2 + Value: 1 + CostsHuman: + RequiredItem: Costume_Ticket diff --git a/db/stylist.yml b/db/stylist.yml new file mode 100644 index 0000000000..f748603581 --- /dev/null +++ b/db/stylist.yml @@ -0,0 +1,47 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +########################################################################### +# Stylist Database +########################################################################### +# +# Stylist Settings +# +########################################################################### +# - Look Look that will be changed. +# Options Possible options to select from. +# - Index Client side index of the option. +# Value Value of the look (can also be an item name). +# CostsHuman: Costs for human players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +# CostsDoram: Costs for doram players. +# Price Required zeny. (Default: 0) +# RequiredItem Required item. (Default: None) +# RequiredItemBox Required item box. (Default: None) +########################################################################### + +Header: + Type: STYLIST_DB + Version: 1 + +Footer: + Imports: + - Path: db/re/stylist.yml + Mode: Renewal + - Path: db/import/stylist.yml diff --git a/doc/atcommands.txt b/doc/atcommands.txt index 366b2ca570..39a57983f0 100644 --- a/doc/atcommands.txt +++ b/doc/atcommands.txt @@ -1237,6 +1237,14 @@ Note: This command requires packet version 2016-10-12 or newer. --------------------------------------- +@stylist + +Opens the stylist user interface. + +Note: This command requires packet version 2015-11-04 or newer. + +--------------------------------------- + @request Sends a message to all connected GMs (via the GM whisper system). diff --git a/doc/script_commands.txt b/doc/script_commands.txt index 42ff1eea56..9d47decbdb 100644 --- a/doc/script_commands.txt +++ b/doc/script_commands.txt @@ -2897,6 +2897,14 @@ This feature requires 2016-10-12aRagexeRE or newer. --------------------------------------- +*openstylist({}) + +Opens the stylist UI for the attached player or the given character id. + +This feature requires packet version 2015-11-04 or newer. + +--------------------------------------- + *getareadropitem("",,,,,) This function will count all the items with the specified ID number lying on the diff --git a/src/map/atcommand.cpp b/src/map/atcommand.cpp index 7feac26420..1c0a2a0a78 100644 --- a/src/map/atcommand.cpp +++ b/src/map/atcommand.cpp @@ -10655,6 +10655,24 @@ ACMD_FUNC(refineui) #endif } +ACMD_FUNC( stylist ){ + nullpo_retr(-1, sd); + +#if PACKETVER < 20151104 + clif_displaymessage( fd, msg_txt( sd, 798 ) ); // This command requires packet version 2015-11-04 or newer. + return -1; +#else + + if( sd->state.stylist_open ){ + clif_displaymessage( fd, msg_txt( sd, 799 ) ); // You have already opened the stylist UI. + return -1; + } + + clif_ui_open( sd, OUT_UI_STYLIST, 0 ); + return 0; +#endif +} + #include "../custom/atcommand.inc" /** @@ -10975,6 +10993,7 @@ void atcommand_basecommands(void) { ACMD_DEF2("completequest", quest), ACMD_DEF2("checkquest", quest), ACMD_DEF(refineui), + ACMD_DEFR(stylist, ATCMD_NOCONSOLE|ATCMD_NOAUTOTRADE), }; AtCommandInfo* atcommand; int i; diff --git a/src/map/clif.cpp b/src/map/clif.cpp index 2ecec70f45..978bf1ae67 100644 --- a/src/map/clif.cpp +++ b/src/map/clif.cpp @@ -21509,6 +21509,13 @@ void clif_parse_changedress( int fd, struct map_session_data* sd ){ void clif_ui_open( struct map_session_data *sd, enum out_ui_type ui_type, int32 data ){ nullpo_retv(sd); + // If the UI requires state tracking + switch( ui_type ){ + case OUT_UI_STYLIST: + sd->state.stylist_open = true; + break; + } + int fd = sd->fd; WFIFOHEAD(fd,packet_len(0xae2)); @@ -22364,6 +22371,172 @@ void clif_parse_unequipall( int fd, struct map_session_data* sd ){ #endif } +void clif_stylist_response( struct map_session_data* sd, bool failed ){ +#if PACKETVER >= 20151104 + struct PACKET_ZC_STYLE_CHANGE_RES p = {}; + + p.PacketType = HEADER_ZC_STYLE_CHANGE_RES; + p.flag = failed; + + clif_send( &p, sizeof( p ), &sd->bl, SELF ); + + if( !failed ){ + sd->state.stylist_open = false; + } +#endif +} + +bool clif_parse_stylist_buy_sub( struct map_session_data* sd, _look look, int16 index ){ + std::shared_ptr list = stylist_db.find( look ); + + if( list == nullptr ){ + return false; + } + + std::shared_ptr entry = util::umap_find( list->entries, index ); + + if( entry == nullptr ){ + return false; + } + + std::shared_ptr costs; + + if( ( sd->class_ & MAPID_BASEMASK ) == MAPID_SUMMONER ){ + costs = entry->doram; + }else{ + costs = entry->human; + } + + if( costs == nullptr ){ + return false; + } + + if( sd->status.zeny < costs->price ){ + return false; + } + + int16 inventoryIndex = -1; + + if( costs->requiredItem != 0 ){ + inventoryIndex = pc_search_inventory( sd, costs->requiredItem ); + + if( inventoryIndex < 0 ){ + // No other option + if( costs->requiredItemBox == 0 ){ + return false; + } + + // Check if the box that contains the item is in the inventory + inventoryIndex = pc_search_inventory( sd, costs->requiredItemBox ); + + // The box containing the item also does not exist + if( inventoryIndex < 0 ){ + return false; + } + } + }else if( costs->requiredItemBox != 0 ){ + inventoryIndex = pc_search_inventory( sd, costs->requiredItem ); + + if( inventoryIndex < 0 ){ + return false; + } + } + + if( inventoryIndex >= 0 && pc_delitem( sd, inventoryIndex, 1, 0, 0, LOG_TYPE_OTHER ) != 0 ){ + return false; + } + + if( costs->price > 0 && pc_payzeny( sd, costs->price, LOG_TYPE_OTHER, nullptr ) != 0 ){ + return false; + } + + switch( look ){ + case LOOK_HAIR: + case LOOK_HAIR_COLOR: + case LOOK_CLOTHES_COLOR: + case LOOK_BODY2: + pc_changelook( sd, look, entry->value ); + break; + case LOOK_HEAD_BOTTOM: + case LOOK_HEAD_MID: + case LOOK_HEAD_TOP: { + struct mail_message msg = {}; + + msg.dest_id = sd->status.char_id; + safestrncpy( msg.send_name, "Styling Shop", NAME_LENGTH ); + safestrncpy( msg.title, "2949", MAIL_TITLE_LENGTH ); + safestrncpy( msg.body, "2950", MAIL_BODY_LENGTH ); + + msg.item[0].nameid = entry->value; + msg.item[0].identify = 1; + msg.item[0].amount = 1; + + msg.status = MAIL_NEW; + msg.type = MAIL_INBOX_NORMAL; + msg.timestamp = time( nullptr ); + + intif_Mail_send( 0, &msg ); + + } break; + } + + return true; +} + +void clif_parse_stylist_buy( int fd, struct map_session_data* sd ){ +#if PACKETVER >= 20151104 +#if PACKETVER >= 20180516 + struct PACKET_CZ_REQ_STYLE_CHANGE2* p = (struct PACKET_CZ_REQ_STYLE_CHANGE2*)RFIFOP( fd, 0 ); +#else + struct PACKET_CZ_REQ_STYLE_CHANGE* p = (struct PACKET_CZ_REQ_STYLE_CHANGE*)RFIFOP( fd, 0 ); +#endif +#endif + if( p->HeadPalette != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_HAIR_COLOR, p->HeadPalette ) ){ + clif_stylist_response( sd, true ); + return; + } + + if( p->HeadStyle != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_HAIR, p->HeadStyle ) ){ + clif_stylist_response( sd, true ); + return; + } + + if( p->BodyPalette != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_CLOTHES_COLOR, p->BodyPalette ) ){ + clif_stylist_response( sd, true ); + return; + } + + if( p->TopAccessory != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_HEAD_TOP, p->TopAccessory ) ){ + clif_stylist_response( sd, true ); + return; + } + + if( p->MidAccessory != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_HEAD_MID, p->MidAccessory ) ){ + clif_stylist_response( sd, true ); + return; + } + + if( p->BottomAccessory != 0 && !clif_parse_stylist_buy_sub( sd, LOOK_HEAD_BOTTOM, p->BottomAccessory ) ){ + clif_stylist_response( sd, true ); + return; + } + +#if PACKETVER >= 20180516 + if( p->BodyStyle != 0 && ( sd->class_ & JOBL_THIRD ) != 0 && ( sd->class_ & JOBL_FOURTH ) == 0 && !clif_parse_stylist_buy_sub( sd, LOOK_BODY2, p->BodyStyle ) ){ + clif_stylist_response( sd, true ); + return; + } +#endif + + clif_stylist_response( sd, false ); +} + +void clif_parse_stylist_close( int fd, struct map_session_data* sd ){ +#if PACKETVER >= 20151104 + sd->state.stylist_open = false; +#endif +} + /*========================================== * Main client packet processing function *------------------------------------------*/ diff --git a/src/map/clif.hpp b/src/map/clif.hpp index 4ddad74006..b0031dbaa3 100644 --- a/src/map/clif.hpp +++ b/src/map/clif.hpp @@ -1135,6 +1135,7 @@ enum in_ui_type : int8 { }; enum out_ui_type : int8 { + OUT_UI_STYLIST = 1, OUT_UI_ATTENDANCE = 7 }; diff --git a/src/map/clif_packetdb.hpp b/src/map/clif_packetdb.hpp index d8f459fa95..efe9f27dff 100644 --- a/src/map/clif_packetdb.hpp +++ b/src/map/clif_packetdb.hpp @@ -2273,6 +2273,11 @@ parseable_packet(0x0980,7,clif_parse_SelectCart,2,6); // CZ_SELECTCART #endif +#if PACKETVER >= 20151104 + parseable_packet( HEADER_CZ_REQ_STYLE_CHANGE, sizeof( PACKET_CZ_REQ_STYLE_CHANGE ), clif_parse_stylist_buy, 0 ); + parseable_packet( HEADER_CZ_REQ_STYLE_CLOSE, sizeof( PACKET_CZ_REQ_STYLE_CLOSE ), clif_parse_stylist_close, 0 ); +#endif + // 2016-03-02bRagexe #if PACKETVER >= 20160302 packet(0x0A51,34); @@ -2394,6 +2399,10 @@ packet(0x0ADD, 22); #endif +#if PACKETVER >= 20180516 + parseable_packet( HEADER_CZ_REQ_STYLE_CHANGE2, sizeof( PACKET_CZ_REQ_STYLE_CHANGE2 ), clif_parse_stylist_buy, 0 ); +#endif + #if PACKETVER_MAIN_NUM >= 20181002 || PACKETVER_RE_NUM >= 20181002 || PACKETVER_ZERO_NUM >= 20181010 parseable_packet( 0x0B10, sizeof( struct PACKET_CZ_START_USE_SKILL ), clif_parse_StartUseSkillToId, 0 ); parseable_packet( 0x0B11, sizeof( struct PACKET_CZ_STOP_USE_SKILL ), clif_parse_StopUseSkillToId, 0 ); diff --git a/src/map/map-server.vcxproj b/src/map/map-server.vcxproj index 2afc0a4fa7..c812d0c6fe 100644 --- a/src/map/map-server.vcxproj +++ b/src/map/map-server.vcxproj @@ -343,5 +343,6 @@ + diff --git a/src/map/npc.cpp b/src/map/npc.cpp index 1d6482b182..be55c72528 100644 --- a/src/map/npc.cpp +++ b/src/map/npc.cpp @@ -121,6 +121,271 @@ struct script_event_s{ // Holds pointers to the commonly executed scripts for speedup. [Skotlex] std::map> script_event; +const std::string StylistDatabase::getDefaultLocation(){ + return std::string(db_path) + "/stylist.yml"; +} + +bool StylistDatabase::parseCostNode( std::shared_ptr entry, bool doram, const YAML::Node& node ){ + std::shared_ptr costs = doram ? entry->doram : entry->human; + bool costs_exists = costs != nullptr; + + if( !costs_exists ){ + costs = std::make_shared(); + } + + if( this->nodeExists( node, "Price" ) ){ + uint32 price; + + if( !this->asUInt32( node, "Price", price ) ){ + return false; + } + + if( price > MAX_ZENY ){ + this->invalidWarning( node["Price"], "stylist_parseCostNode: Price %u is too high, capping to MAX_ZENY...\n", price ); + price = MAX_ZENY; + } + + costs->price = price; + }else{ + if( !costs_exists ){ + costs->price = 0; + } + } + + if( this->nodeExists( node, "RequiredItem" ) ){ + std::string item; + + if( !this->asString( node, "RequiredItem", item ) ){ + return false; + } + + std::shared_ptr id = item_db.search_aegisname( item.c_str() ); + + if( id == nullptr ){ + this->invalidWarning( node["RequiredItem"], "stylist_parseCostNode: Unknown item \"%s\"...\n", item.c_str() ); + return false; + } + + costs->requiredItem = id->nameid; + }else{ + if( !costs_exists ){ + costs->requiredItem = 0; + } + } + + if( this->nodeExists( node, "RequiredItemBox" ) ){ + std::string item; + + if( !this->asString( node, "RequiredItemBox", item ) ){ + return false; + } + + std::shared_ptr id = item_db.search_aegisname( item.c_str() ); + + if( id == nullptr ){ + this->invalidWarning( node["RequiredItemBox"], "stylist_parseCostNode: Unknown item \"%s\"...\n", item.c_str() ); + return false; + } + + costs->requiredItemBox = id->nameid; + }else{ + if( !costs_exists ){ + costs->requiredItemBox = 0; + } + } + + if( !costs_exists ){ + if( doram ){ + entry->doram = costs; + }else{ + entry->human = costs; + } + } + + return true; +} + +uint64 StylistDatabase::parseBodyNode( const YAML::Node &node ){ + if( !this->nodesExist( node, { "Look", "Options" } ) ){ + return 0; + } + + std::string look_str; + + if( !this->asString( node, "Look", look_str ) ){ + return 0; + } + + int64 constant; + + if( !script_get_constant( ( "LOOK_" + look_str ).c_str(), &constant ) ){ + this->invalidWarning( node["Look"], "stylist_parseBodyNode: Invalid look %s.\n", look_str.c_str() ); + return 0; + } + + switch( constant ){ + case LOOK_HEAD_TOP: + case LOOK_HEAD_MID: + case LOOK_HEAD_BOTTOM: + case LOOK_HAIR: + case LOOK_HAIR_COLOR: + case LOOK_CLOTHES_COLOR: + case LOOK_BODY2: + break; + default: + this->invalidWarning( node["Look"], "stylist_parseBodyNode: Unsupported look value \"%s\"...\n", look_str.c_str() ); + return 0; + } + + std::shared_ptr list = this->find( (uint32)constant ); + bool exists = list != nullptr; + uint64 count = 0; + + if( !exists ){ + list = std::make_shared(); + list->look = (uint16)constant; + } + + for( const YAML::Node& optionNode : node["Options"] ){ + int16 index; + + if( !this->asInt16( optionNode, "Index", index ) ){ + return 0; + } + + if( index == 0 ){ + this->invalidWarning( optionNode["Index"], "stylist_parseBodyNode: Unsupported index value \"%hd\"...\n", index ); + return 0; + } + + std::shared_ptr entry = util::umap_find( list->entries, index ); + bool entry_exists = entry != nullptr; + + if( !entry_exists ){ + entry = std::make_shared(); + entry->look = list->look; + entry->index = index; + + if( !this->nodesExist( optionNode, { "Value" } ) ){ + return 0; + } + } + + if( this->nodeExists( optionNode, "Value" ) ){ + uint32 value; + + switch( list->look ){ + case LOOK_HEAD_TOP: + case LOOK_HEAD_MID: + case LOOK_HEAD_BOTTOM: { + std::string item; + + if( !this->asString( optionNode, "Value", item ) ){ + return 0; + } + + std::shared_ptr id = item_db.search_aegisname( item.c_str() ); + + if( id == nullptr ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: Unknown item \"%s\"...\n", item.c_str() ); + return 0; + } + + value = id->nameid; + } break; + case LOOK_HAIR: + if( !this->asUInt32( optionNode, "Value", value ) ){ + return 0; + } + + if( value < MIN_HAIR_STYLE ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: hair style \"%u\" is too low...\n", value ); + return 0; + }else if( value > MAX_HAIR_STYLE ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: hair style \"%u\" is too high...\n", value ); + return 0; + } + break; + case LOOK_HAIR_COLOR: + if( !this->asUInt32( optionNode, "Value", value ) ){ + return 0; + } + + if( value < MIN_HAIR_COLOR ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: hair color \"%u\" is too low...\n", value ); + return 0; + }else if( value > MAX_HAIR_COLOR ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: hair color \"%u\" is too high...\n", value ); + return 0; + } + break; + case LOOK_CLOTHES_COLOR: + if( !this->asUInt32( optionNode, "Value", value ) ){ + return 0; + } + + if( value < MIN_CLOTH_COLOR ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: cloth color \"%u\" is too low...\n", value ); + return 0; + }else if( value > MAX_CLOTH_COLOR ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: cloth color \"%u\" is too high...\n", value ); + return 0; + } + break; + case LOOK_BODY2: + if( !this->asUInt32( optionNode, "Value", value ) ){ + return 0; + } + + if( value < MIN_BODY_STYLE ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: body style \"%u\" is too low...\n", value ); + return 0; + }else if( value > MAX_BODY_STYLE ){ + this->invalidWarning( optionNode["Value"], "stylist_parseBodyNode: body style \"%u\" is too high...\n", value ); + return 0; + } + break; + } + + entry->value = value; + } + + if( this->nodeExists( optionNode, "CostsHuman" ) ) { + if( !this->parseCostNode( entry, false, optionNode["CostsHuman"] ) ){ + return 0; + } + }else{ + if( !entry_exists ){ + entry->human = nullptr; + } + } + + if( this->nodeExists( optionNode, "CostsDoram" ) ) { + if( !this->parseCostNode( entry, true, optionNode["CostsDoram"] ) ){ + return 0; + } + }else{ + if( !entry_exists ){ + entry->doram = nullptr; + } + } + + if( !entry_exists ){ + list->entries[index] = entry; + } + + count++; + } + + if( !exists ){ + this->put( (uint32)constant, list ); + } + + return count; +} + +StylistDatabase stylist_db; + /** * Returns the viewdata for normal NPC classes. * @param class_: NPC class ID @@ -4785,6 +5050,8 @@ int npc_reload(void) { "\t-'" CL_WHITE "%d" CL_RESET "' Mobs Not Cached\n", npc_id - npc_new_min, npc_warp, npc_shop, npc_script, npc_mob, npc_cache_mob, npc_delay_mob); + stylist_db.reload(); + //Re-read the NPC Script Events cache. npc_read_event_script(); @@ -4851,6 +5118,7 @@ void do_final_npc(void) { #if PACKETVER >= 20131223 NPCMarketDB->destroy(NPCMarketDB, npc_market_free); #endif + stylist_db.clear(); ers_destroy(timer_event_ers); ers_destroy(npc_sc_display_ers); npc_clearsrcfile(); @@ -4936,6 +5204,8 @@ void do_init_npc(void){ "\t-'" CL_WHITE "%d" CL_RESET "' Mobs Not Cached\n", npc_id - START_NPC_NUM, npc_warp, npc_shop, npc_script, npc_mob, npc_cache_mob, npc_delay_mob); + stylist_db.load(); + // set up the events cache npc_read_event_script(); diff --git a/src/map/npc.hpp b/src/map/npc.hpp index 0f2e12f5c3..718a3bd83d 100644 --- a/src/map/npc.hpp +++ b/src/map/npc.hpp @@ -51,6 +51,40 @@ struct s_npc_buy_list { #pragma pack(pop) #endif // not NetBSD < 6 / Solaris +struct s_stylist_costs{ + uint32 price; + t_itemid requiredItem; + t_itemid requiredItemBox; +}; + +struct s_stylist_entry{ + uint16 look; + int16 index; + uint32 value; + std::shared_ptr human; + std::shared_ptr doram; +}; + +struct s_stylist_list{ + uint16 look; + std::unordered_map> entries; +}; + +class StylistDatabase : public TypesafeYamlDatabase{ +private: + bool parseCostNode( std::shared_ptr entry, bool doram, const YAML::Node& node ); + +public: + StylistDatabase() : TypesafeYamlDatabase( "STYLIST_DB", 1 ){ + + } + + const std::string getDefaultLocation(); + uint64 parseBodyNode( const YAML::Node& node ); +}; + +extern StylistDatabase stylist_db; + struct s_questinfo { e_questinfo_types icon; e_questinfo_markcolor color; diff --git a/src/map/packets.hpp b/src/map/packets.hpp index 0c5c3f9acf..1a0ef40d92 100644 --- a/src/map/packets.hpp +++ b/src/map/packets.hpp @@ -222,6 +222,10 @@ struct PACKET_CZ_UNCONFIRMED_RODEX_RETURN{ uint32 msgId; } __attribute__((packed)); +struct PACKET_CZ_REQ_STYLE_CLOSE{ + int16 packetType; +} __attribute__((packed)); + // NetBSD 5 and Solaris don't like pragma pack but accept the packed attribute #if !defined( sun ) && ( !defined( __NETBSD__ ) || __NetBSD_Version__ >= 600000000 ) #pragma pack( pop ) @@ -275,6 +279,10 @@ DEFINE_PACKET_HEADER(ZC_ACK_COUNT_BARGAIN_SALE_ITEM, 0x9c4) DEFINE_PACKET_HEADER(ZC_ACK_GUILDSTORAGE_LOG, 0x9da) DEFINE_PACKET_HEADER(CZ_NPC_MARKET_PURCHASE, 0x9d6) DEFINE_PACKET_HEADER(CZ_REQ_APPLY_BARGAIN_SALE_ITEM2, 0xa3d) +DEFINE_PACKET_HEADER(CZ_REQ_STYLE_CHANGE, 0xa46) +DEFINE_PACKET_HEADER(ZC_STYLE_CHANGE_RES, 0xa47) +DEFINE_PACKET_HEADER(CZ_REQ_STYLE_CLOSE, 0xa48) +DEFINE_PACKET_HEADER(CZ_REQ_STYLE_CHANGE2, 0xafc) DEFINE_PACKET_HEADER(ZC_REMOVE_EFFECT, 0x0b0d) DEFINE_PACKET_HEADER(CZ_UNCONFIRMED_TSTATUS_UP, 0x0b24) DEFINE_PACKET_HEADER(CZ_GUILD_EMBLEM_CHANGE2, 0x0b46) diff --git a/src/map/pc.hpp b/src/map/pc.hpp index 982e9f3927..2644e5068d 100644 --- a/src/map/pc.hpp +++ b/src/map/pc.hpp @@ -382,6 +382,7 @@ struct map_session_data { bool mail_writing; // Whether the player is currently writing a mail in RODEX or not bool cashshop_open; bool sale_open; + bool stylist_open; unsigned int block_action : 10; bool refineui_open; } state; @@ -1049,9 +1050,15 @@ extern JobDatabase job_db; #define pc_isidle_hom(sd) ( (sd)->hd && ( (sd)->chatID || (sd)->state.vending || (sd)->state.buyingstore || DIFF_TICK(last_tick, (sd)->idletime_hom) >= battle_config.hom_idle_no_share ) ) #define pc_isidle_mer(sd) ( (sd)->md && ( (sd)->chatID || (sd)->state.vending || (sd)->state.buyingstore || DIFF_TICK(last_tick, (sd)->idletime_mer) >= battle_config.mer_idle_no_share ) ) #define pc_istrading(sd) ( (sd)->npc_id || (sd)->state.vending || (sd)->state.buyingstore || (sd)->state.trading ) +static bool pc_cant_act2( struct map_session_data* sd ){ + return sd->state.vending || sd->state.buyingstore || (sd->sc.opt1 && sd->sc.opt1 != OPT1_BURNING) + || sd->state.trading || sd->state.storage_flag || sd->state.prevend || sd->state.refineui_open + || sd->state.stylist_open; +} // equals pc_cant_act2 and additionally checks for chat rooms and npcs -#define pc_cant_act(sd) ( (sd)->npc_id || (sd)->chatID || pc_cant_act2( (sd) ) ) -#define pc_cant_act2(sd) ( (sd)->state.vending || (sd)->state.buyingstore || ((sd)->sc.opt1 && (sd)->sc.opt1 != OPT1_BURNING) || (sd)->state.trading || (sd)->state.storage_flag || (sd)->state.prevend || (sd)->state.refineui_open ) +static bool pc_cant_act( struct map_session_data* sd ){ + return sd->npc_id || sd->chatID || pc_cant_act2( sd ); +} #define pc_setdir(sd,b,h) ( (sd)->ud.dir = (b) ,(sd)->head_dir = (h) ) #define pc_setchatid(sd,n) ( (sd)->chatID = n ) diff --git a/src/map/script.cpp b/src/map/script.cpp index e45e3b1c7e..bc925a2d3e 100644 --- a/src/map/script.cpp +++ b/src/map/script.cpp @@ -25587,6 +25587,23 @@ BUILDIN_FUNC(mob_setidleevent){ return SCRIPT_CMD_SUCCESS; } +BUILDIN_FUNC( openstylist ){ +#if PACKETVER >= 20151104 + struct map_session_data* sd; + + if( !script_charid2sd( 2, sd ) ){ + return SCRIPT_CMD_FAILURE; + } + + clif_ui_open( sd, OUT_UI_STYLIST, 0 ); + + return SCRIPT_CMD_SUCCESS; +#else + ShowError( "buildin_openstylist: This command requires packet version 2015-11-04 or newer.\n" ); + return SCRIPT_CMD_FAILURE; +#endif +} + #include "../custom/script.inc" // declarations that were supposed to be exported from npc_chat.cpp @@ -26291,6 +26308,7 @@ struct script_function buildin_func[] = { BUILDIN_DEF(mob_setidleevent, "is"), BUILDIN_DEF(setinstancevar,"rvi"), + BUILDIN_DEF(openstylist, "?"), #include "../custom/script_def.inc" {NULL,NULL,NULL},