From ec9a5fae4b391fa1fc3ff73bdc643136de488a94 Mon Sep 17 00:00:00 2001 From: Playtester <3785983+Playtester@users.noreply.github.com> Date: Fri, 7 Jun 2024 18:29:01 +0200 Subject: [PATCH] Monster Loot / Item Dropping Position (#8347) - When a monster is killed, its first item will now always drop at the exact cell the monster was on - When a monster drops more than one item, the items will be deployed on 3 cells around the monster (looping): SE, W and N - Fixed drop position of looted items (similar to regular drops but independent to it and starts north) - Fixed item drop order (script-granted -> regular drops -> looted drops) - Fixed looted items showing special drop effects - Searching for a free cell to drop an item on now uses the official algorithm - When a monster drops an item, it will no longer drop on cells that are occupied by characters or pets - When a player drops an item, it will now drop in a 5x5 area around the player - Items dropped by players can now stack on the same cell unless the new "item_stacking" config is disabled - When an MVP drop drops to the floor because the player's inventory was full, it will now always drop on that player's cell - Fixes #8345 --- conf/battle/misc.conf | 7 ++ src/map/battle.cpp | 1 + src/map/battle.hpp | 1 + src/map/map.cpp | 114 +++++++++++++++++++--------- src/map/map.hpp | 3 +- src/map/mob.cpp | 171 ++++++++++++++++++++++++------------------ src/map/pc.cpp | 7 +- 7 files changed, 191 insertions(+), 113 deletions(-) diff --git a/conf/battle/misc.conf b/conf/battle/misc.conf index a16a496f5a..145b0a9872 100644 --- a/conf/battle/misc.conf +++ b/conf/battle/misc.conf @@ -190,3 +190,10 @@ hide_fav_sell: no // affects teleportation. Set this to 1 if you want it to be closer to the old emulator behavior. // Valid values: 1-40 map_edge_size: 15 + +// When a player drops items, can they stack on the same cell? (Note 1) +// Officially there's no limit on how many items you can drop on the same cell. +// If you set this to "no", when you drop an item, it will only drop on a cell that has no item on it yet. +// A free cell will be searched for in eight directions. If no free cell could be found in those eight tries, +// then dropping the item will fail (the item stays in the player's inventory). +item_stacking: yes diff --git a/src/map/battle.cpp b/src/map/battle.cpp index 6ce0aecd2a..cb67881c3d 100644 --- a/src/map/battle.cpp +++ b/src/map/battle.cpp @@ -11524,6 +11524,7 @@ static const struct _battle_data { { "feature.instance_allow_reconnect", &battle_config.instance_allow_reconnect, 0, 0, 1, }, #endif { "synchronize_damage", &battle_config.synchronize_damage, 0, 0, 1, }, + { "item_stacking", &battle_config.item_stacking, 1, 0, 1, }, #include }; diff --git a/src/map/battle.hpp b/src/map/battle.hpp index 7b62ca1fee..e7ec5b957a 100644 --- a/src/map/battle.hpp +++ b/src/map/battle.hpp @@ -756,6 +756,7 @@ struct Battle_Config int feature_banking_state_enforce; int instance_allow_reconnect; int synchronize_damage; + int item_stacking; #include }; diff --git a/src/map/map.cpp b/src/map/map.cpp index 7ddaf3322a..75f27ce3cf 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -1644,43 +1644,71 @@ void map_clearflooritem(struct block_list *bl) { map_freeblock(&fitem->bl); } -/*========================================== - * (m,x,y) locates a random available free cell around the given coordinates - * to place an BL_ITEM object. Scan area is 9x9, returns 1 on success. - * x and y are modified with the target cell when successful. - *------------------------------------------*/ -int map_searchrandfreecell(int16 m,int16 *x,int16 *y,int stack) { - int free_cell,i,j; - int free_cells[9][2]; - struct map_data *mapdata = map_getmapdata(m); - - if( mapdata == nullptr || mapdata->block == nullptr ){ - return 0; +/** + * Returns if a cell is passable and not occupied by given types of block + * Cells 5 cells from the SW edge and 4 cells from the NE edge are never considered as free + * @param m: Map of cell to check + * @param x: X-coordinate of cell to check + * @param y: Y-coordinate of cell to check + * @param type: Types of block to check for + * @return True if cell is passable, not on the edge and not occupied by given types of block + */ +bool map_cell_free(int16 m, int16 x, int16 y, int type) +{ + struct map_data* mapdata = map_getmapdata(m); + if (mapdata == nullptr || mapdata->block == nullptr) { + return false; } - for(free_cell=0,i=-1;i<=1;i++){ - if(i+*y<0 || i+*y>=mapdata->ys) - continue; - for(j=-1;j<=1;j++){ - if(j+*x<0 || j+*x>=mapdata->xs) - continue; - if(map_getcell(m,j+*x,i+*y,CELL_CHKNOPASS) && !map_getcell(m,j+*x,i+*y,CELL_CHKICEWALL)) - continue; - //Avoid item stacking to prevent against exploits. [Skotlex] - if(stack && map_count_oncell(m,j+*x,i+*y, BL_ITEM, 0) > stack) - continue; - free_cells[free_cell][0] = j+*x; - free_cells[free_cell++][1] = i+*y; - } - } - if(free_cell==0) - return 0; - free_cell = rnd_value(0, free_cell-1); - *x = free_cells[free_cell][0]; - *y = free_cells[free_cell][1]; - return 1; + // Cells outside the map or within 4-5 cells of the map edge are considered invalid officially + // Note that this isn't symmetric (NE - 4 cells, SW - 5 cells) + // If for some reason edge size was set to below 5 cells, we consider them as valid + int16 edge_valid = std::min(battle_config.map_edge_size, 5); + if (x < edge_valid || x > mapdata->xs - edge_valid || y < edge_valid || y > mapdata->ys - edge_valid) + return false; + if (map_getcell(m, x, y, CELL_CHKNOPASS)) + return false; + if (map_count_oncell(m, x, y, type, 0) > 0) + return false; + + return true; } +/** + * Locates a random available free cell around the given coordinates within a given distance range. + * This uses the official algorithm that checks each quadrant and line once. + * x and y are modified with the target free cell when successful. + * @param m: Map to search + * @param x: X-coordinate around which free cell is searched + * @param y: Y-coordinate around which free cell is searched + * @param distmin: Minimum distance from the given cell + * @param distmax: Maximum distance from the given cell + * @param type: If the given types of block are present on the cell, it counts as occupied + * @return True if free cell could be found + */ +bool map_search_freecell_dist(int16 m, int16* x, int16* y, int16 distmin, int16 distmax, int type) +{ + // This is to prevent that always the same quadrant is checked first + int16 mirrorx = (rnd()%2) ? -1 : 1; + int16 mirrory = (rnd()%2) ? -1 : 1; + + for (int16 i = -1; i <= 1; i++) { + for (int16 j = -1; j <= 1; j++) { + if (i || j) + { + int16 checkX = *x + mirrorx * i * rnd_value(distmin, distmax); + int16 checkY = *y + mirrory * j * rnd_value(distmin, distmax); + if (map_cell_free(m, checkX, checkY, type)) + { + *x = checkX; + *y = checkY; + return true; + } + } + } + } + return false; +} static int map_count_sub(struct block_list *bl,va_list ap) { @@ -1860,12 +1888,14 @@ bool map_closest_freecell(int16 m, int16 *x, int16 *y, int type, int flag) * @param first_charid : 1st player that could loot the item (only charid that could loot for first_get_tick duration) * @param second_charid : 2nd player that could loot the item (2nd charid that could loot for second_get_charid duration) * @param third_charid : 3rd player that could loot the item (3rd charid that could loot for third_get_charid duration) - * @param flag: &1 MVP item. &2 do stacking check. &4 bypass droppable check. + * @param flag: &1 MVP item. &2 search free cell in 5x5 area instead of 3x3. &4 bypass droppable check. * @param mob_id: Monster ID if dropped by monster * @param canShowEffect: enable pillar effect on the dropped item (if set in the database) + * @param dir: where the item should drop around the target (DIR_MAX: random cell around center) + * @param type: types of block the item should not stack on * @return 0:failure, x:item_gid [MIN_FLOORITEM;MAX_FLOORITEM]==[2;START_ACCOUNT_NUM] *------------------------------------------*/ -int map_addflooritem(struct item *item, int amount, int16 m, int16 x, int16 y, int first_charid, int second_charid, int third_charid, int flags, unsigned short mob_id, bool canShowEffect) +int map_addflooritem(struct item *item, int amount, int16 m, int16 x, int16 y, int first_charid, int second_charid, int third_charid, int flags, unsigned short mob_id, bool canShowEffect, enum directions dir, int type) { struct flooritem_data *fitem = nullptr; @@ -1874,8 +1904,18 @@ int map_addflooritem(struct item *item, int amount, int16 m, int16 x, int16 y, i if (!(flags&4) && battle_config.item_onfloor && (itemdb_traderight(item->nameid).trade)) return 0; //can't be dropped - if (!map_searchrandfreecell(m,&x,&y,flags&2?1:0)) - return 0; + if (dir > DIR_CENTER && dir < DIR_MAX) { + x += dirx[dir]; + y += diry[dir]; + } + // If cell occupied and not center cell, drop item around the drop target cell + if (dir == DIR_MAX || (dir != DIR_CENTER && !map_cell_free(m, x, y, type))) { + if (!map_search_freecell_dist(m, &x, &y, 1, (flags&2)?2:1, type)) { + // Only stop here if BL_ITEM shall not stack, otherwise drop on original target cell + if (type&BL_ITEM) + return 0; + } + } CREATE(fitem, struct flooritem_data, 1); fitem->bl.type=BL_ITEM; diff --git a/src/map/map.hpp b/src/map/map.hpp index f8204f88d2..c684b1e28d 100644 --- a/src/map/map.hpp +++ b/src/map/map.hpp @@ -21,6 +21,7 @@ #include "navi.hpp" #include "script.hpp" +#include "path.hpp" using rathena::server_core::Core; using rathena::server_core::e_core_type; @@ -1137,7 +1138,7 @@ bool map_addnpc(int16 m,struct npc_data *); TIMER_FUNC(map_clearflooritem_timer); TIMER_FUNC(map_removemobs_timer); void map_clearflooritem(struct block_list* bl); -int map_addflooritem(struct item *item, int amount, int16 m, int16 x, int16 y, int first_charid, int second_charid, int third_charid, int flags, unsigned short mob_id, bool canShowEffect = false); +int map_addflooritem(struct item *item, int amount, int16 m, int16 x, int16 y, int first_charid, int second_charid, int third_charid, int flags, unsigned short mob_id, bool canShowEffect = false, enum directions dir = DIR_MAX, int type = BL_NUL); // instances int map_addinstancemap(int src_m, int instance_id, bool no_mapflag); diff --git a/src/map/mob.cpp b/src/map/mob.cpp index 091b2e6408..521fd7237c 100644 --- a/src/map/mob.cpp +++ b/src/map/mob.cpp @@ -81,6 +81,7 @@ struct s_mob_skill_db { std::unordered_map> mob_skill_db; /// Monster skill temporary db. s_mob_skill_db -> mobid std::unordered_map> mob_delayed_drops; +std::unordered_map> mob_looted_drops; MobSummonDatabase mob_summon_db; MobChatDatabase mob_chat_db; MapDropDatabase map_drop_db; @@ -2216,25 +2217,53 @@ static std::shared_ptr mob_setlootitem( s_mob_lootitem& item, unsig return drop; } +/** + * Makes all items from a drop list drop + * @param list: list with all items that should drop + * @param loot: whether the items in the list are new drops or previously looted items + */ +void mob_process_drop_list(std::shared_ptr& list, bool loot) +{ + // First regular drop always drops at center + enum directions dir = DIR_CENTER; + // Looted drops start north instead + if (loot) + dir = DIR_NORTH; + + for (std::shared_ptr& ditem : list->items) { + map_addflooritem(&ditem->item_data, ditem->item_data.amount, + list->m, list->x, list->y, + list->first_charid, list->second_charid, list->third_charid, 4, ditem->mob_id, !loot, dir, BL_CHAR|BL_PET); + // The drop location loops between three locations: SE -> W -> N -> SE + if (dir <= DIR_NORTH) + dir = DIR_SOUTHEAST; + else if (dir == DIR_SOUTHEAST) + dir = DIR_WEST; + else + dir = DIR_NORTH; + } +} + /*========================================== * item drop with delay (timer function) *------------------------------------------*/ -static TIMER_FUNC(mob_delay_item_drop){ - uint32 bl_id = static_cast( id ); - std::shared_ptr list = util::umap_find( mob_delayed_drops, bl_id ); +static TIMER_FUNC(mob_delay_item_drop) { + uint32 bl_id = static_cast(id); - if( list == nullptr ){ - return 0; + // Regular drops + std::shared_ptr list = util::umap_find(mob_delayed_drops, bl_id); + if (list != nullptr) { + mob_process_drop_list(list, false); + mob_delayed_drops.erase(bl_id); } - for( std::shared_ptr& ditem : list->items ){ - map_addflooritem(&ditem->item_data,ditem->item_data.amount, - list->m,list->x,list->y, - list->first_charid,list->second_charid,list->third_charid,4,ditem->mob_id,true); + // Looted drops + list = util::umap_find(mob_looted_drops, bl_id); + if (list != nullptr) { + mob_process_drop_list(list, true); + mob_looted_drops.erase(bl_id); } - mob_delayed_drops.erase( bl_id ); - return 0; } @@ -2833,6 +2862,24 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) } //End EXP giving. + // Looted items have an independent drop position and also don't show special effects when dropped + // So we need put them into a separate list + std::shared_ptr lootlist = std::make_shared(); + lootlist->m = md->bl.m; + lootlist->x = md->bl.x; + lootlist->y = md->bl.y; + lootlist->first_charid = (mvp_sd ? mvp_sd->status.char_id : 0); + lootlist->second_charid = (second_sd ? second_sd->status.char_id : 0); + lootlist->third_charid = (third_sd ? third_sd->status.char_id : 0); + + // Process items looted by the mob + if (md->lootitems) { + for (i = 0; i < md->lootitem_count; i++) { + std::shared_ptr ditem = mob_setlootitem(md->lootitems[i], md->mob_id); + mob_item_drop(md, lootlist, ditem, 1, 10000, homkillonly || merckillonly); + } + } + if( !(type&1) && !map_getmapflag(m, MF_NOMOBLOOT) && !md->state.rebirth && ( !md->special_state.ai || //Non special mob battle_config.alchemist_summon_reward == 2 || //All summoned give drops @@ -2846,7 +2893,6 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) #endif std::shared_ptr dlist = std::make_shared(); - dlist->m = md->bl.m; dlist->x = md->bl.x; dlist->y = md->bl.y; @@ -2854,40 +2900,6 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) dlist->second_charid = (second_sd ? second_sd->status.char_id : 0); dlist->third_charid = (third_sd ? third_sd->status.char_id : 0); - for (i = 0; i < MAX_MOB_DROP_TOTAL; i++) { - if (md->db->dropitem[i].nameid == 0) - continue; - - std::shared_ptr it = item_db.find(md->db->dropitem[i].nameid); - - if ( it == nullptr ) - continue; - - drop_rate = mob_getdroprate(src, md->db, md->db->dropitem[i].rate, drop_modifier, md); - - // attempt to drop the item - if (rnd() % 10000 >= drop_rate) - continue; - - if( mvp_sd && it->type == IT_PETEGG ) { - pet_create_egg(mvp_sd, md->db->dropitem[i].nameid); - continue; - } - - std::shared_ptr ditem = mob_setdropitem( md->db->dropitem[i], 1, md->mob_id ); - - //A Rare Drop Global Announce by Lupus - if( mvp_sd && md->db->dropitem[i].rate <= battle_config.rare_drop_announce ) { - char message[128]; - sprintf (message, msg_txt(nullptr,541), mvp_sd->status.name, md->name, it->ename.c_str(), (float)drop_rate/100); - //MSG: "'%s' won %s's %s (chance: %0.02f%%)" - intif_broadcast(message,strlen(message)+1,BC_DEFAULT); - } - // Announce first, or else ditem will be freed. [Lance] - // By popular demand, use base drop rate for autoloot code. [Skotlex] - mob_item_drop(md, dlist, ditem, 0, battle_config.autoloot_adjust ? drop_rate : md->db->dropitem[i].rate, homkillonly || merckillonly); - } - // Ore Discovery [Celest] if (sd == mvp_sd && pc_checkskill(sd,BS_FINDINGORE)>0 && battle_config.finding_ore_rate/10 >= rnd()%10000) { s_mob_drop mobdrop = {}; @@ -2943,13 +2955,39 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) } } - // process items looted by the mob - if (md->lootitems) { - for (i = 0; i < md->lootitem_count; i++) { - std::shared_ptr ditem = mob_setlootitem(md->lootitems[i], md->mob_id); + // Regular mob drops drop after script-granted drops + for (i = 0; i < MAX_MOB_DROP_TOTAL; i++) { + if (md->db->dropitem[i].nameid == 0) + continue; - mob_item_drop( md, dlist, ditem, 1, 10000, homkillonly || merckillonly ); + std::shared_ptr it = item_db.find(md->db->dropitem[i].nameid); + + if (it == nullptr) + continue; + + drop_rate = mob_getdroprate(src, md->db, md->db->dropitem[i].rate, drop_modifier, md); + + // attempt to drop the item + if (rnd() % 10000 >= drop_rate) + continue; + + if (mvp_sd && it->type == IT_PETEGG) { + pet_create_egg(mvp_sd, md->db->dropitem[i].nameid); + continue; } + + std::shared_ptr ditem = mob_setdropitem(md->db->dropitem[i], 1, md->mob_id); + + //A Rare Drop Global Announce by Lupus + if (mvp_sd && md->db->dropitem[i].rate <= battle_config.rare_drop_announce) { + char message[128]; + sprintf(message, msg_txt(nullptr, 541), mvp_sd->status.name, md->name, it->ename.c_str(), (float)drop_rate / 100); + //MSG: "'%s' won %s's %s (chance: %0.02f%%)" + intif_broadcast(message, strlen(message) + 1, BC_DEFAULT); + } + // Announce first, or else ditem will be freed. [Lance] + // By popular demand, use base drop rate for autoloot code. [Skotlex] + mob_item_drop(md, dlist, ditem, 0, battle_config.autoloot_adjust ? drop_rate : md->db->dropitem[i].rate, homkillonly || merckillonly); } // Process map specific drops @@ -2989,29 +3027,16 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) } // There are drop items. - if( !dlist->items.empty() ){ + if (!dlist->items.empty() || !lootlist->items.empty()) { mob_delayed_drops[md->bl.id] = dlist; - - add_timer( tick + ( !battle_config.delay_battle_damage ? 500 : 0 ), mob_delay_item_drop, md->bl.id, 0 ); + mob_looted_drops[md->bl.id] = lootlist; + add_timer(tick + (!battle_config.delay_battle_damage ? 500 : 0), mob_delay_item_drop, md->bl.id, 0); } - } else if (md->lootitems && md->lootitem_count) { //Loot MUST drop! - std::shared_ptr dlist = std::make_shared(); - - dlist->m = md->bl.m; - dlist->x = md->bl.x; - dlist->y = md->bl.y; - dlist->first_charid = (mvp_sd ? mvp_sd->status.char_id : 0); - dlist->second_charid = (second_sd ? second_sd->status.char_id : 0); - dlist->third_charid = (third_sd ? third_sd->status.char_id : 0); - - for (i = 0; i < md->lootitem_count; i++) { - std::shared_ptr ditem = mob_setlootitem(md->lootitems[i], md->mob_id); - - mob_item_drop( md, dlist, ditem, 1, 10000, homkillonly || merckillonly ); - } - - mob_delayed_drops[md->bl.id] = dlist; - add_timer( tick + ( !battle_config.delay_battle_damage ? 500 : 0 ), mob_delay_item_drop, md->bl.id, 0 ); + } + // Loot MUST drop! + else if (!lootlist->items.empty()) { + mob_looted_drops[md->bl.id] = lootlist; + add_timer(tick + (!battle_config.delay_battle_damage ? 500 : 0), mob_delay_item_drop, md->bl.id, 0); } if( mvp_sd && md->get_bosstype() == BOSSTYPE_MVP ){ @@ -3114,7 +3139,7 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type) if((temp = pc_additem(mvp_sd,&item,1,LOG_TYPE_PICKDROP_PLAYER)) != 0) { clif_additem(mvp_sd,0,0,temp); - map_addflooritem(&item,1,mvp_sd->bl.m,mvp_sd->bl.x,mvp_sd->bl.y,mvp_sd->status.char_id,(second_sd?second_sd->status.char_id:0),(third_sd?third_sd->status.char_id:0),1,0,true); + map_addflooritem(&item,1,mvp_sd->bl.m,mvp_sd->bl.x,mvp_sd->bl.y,mvp_sd->status.char_id,(second_sd?second_sd->status.char_id:0),(third_sd?third_sd->status.char_id:0),1,0,true,DIR_CENTER); } if (i_data->flag.broadcast) diff --git a/src/map/pc.cpp b/src/map/pc.cpp index 46f28535cf..98ba8a2076 100755 --- a/src/map/pc.cpp +++ b/src/map/pc.cpp @@ -6052,9 +6052,12 @@ bool pc_dropitem(map_session_data *sd,int n,int amount) return false; } - // bypass drop restriction in map_addflooritem because we've already checked it above - if (!map_addflooritem(&sd->inventory.u.items_inventory[n], amount, sd->bl.m, sd->bl.x, sd->bl.y, 0, 0, 0, 2|4, 0)) + // Bypass drop restriction in map_addflooritem because we've already checked it above + if (!map_addflooritem(&sd->inventory.u.items_inventory[n], amount, sd->bl.m, sd->bl.x, sd->bl.y, 0, 0, 0, 2|4, 0, + false, DIR_MAX, battle_config.item_stacking?BL_NUL:BL_ITEM)) + { return false; + } pc_delitem(sd, n, amount, 1, 0, LOG_TYPE_PICKDROP_PLAYER); clif_dropitem( *sd, n, amount );