Implemented dynamic NPCs (#7402)

This is required for features like #6640
Added COLOR_LIGHT_YELLOW
Cleaned up color definitions, its useless to store them as string first.
Dynamic NPCs will stay when you switch map and come back in time.
Dynamic NPCs will be removed instantly when you log out.
You can only create one dynamic NPC at a time.

Thanks to @Atemo!
This commit is contained in:
Lemongrass3110 2022-11-07 00:41:10 +01:00 committed by GitHub
parent 774d4ca2e8
commit a763ad0629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 265 additions and 24 deletions

View File

@ -56,7 +56,7 @@ feature.autotrade_head_direction: 0
// 1 = Sitting
feature.autotrade_sit: 1
// Delay in miliseconds to open vending/buyingsotre after player logged in.
// Delay in milliseconds to open vending/buyingsotre after player logged in.
feature.autotrade_open_delay: 5000
// Battlegrounds queue interface. Makes it possible to queue for a battleground anywhere using the battle menu.
@ -119,3 +119,19 @@ feature.barter: on
// Extended Barter Shop System (Note 1)
// Requires: 2019-11-06RagexeRE or later
feature.barter_extended: on
// The timeout in milliseconds when a dynamic NPC will be despawned if not used.
// Default: 60000 (60s)
feature.dynamicnpc_timeout: 60000
// The x range in which the dynamic NPC will be spawned relative to the player.
// Default: 2
feature.dynamicnpc_rangex: 2
// The y range in which the dynamic NPC will be spawned relative to the player.
// Default: 2
feature.dynamicnpc_rangey: 2
// Should the dynamic NPCs look into the direction of the player? (Note 1)
// Default: no
feature.dynamicnpc_direction: no

View File

@ -6895,6 +6895,17 @@ This command will duplicate the NPC with the given <NPC name> on <map> at <x>/<y
If <Duplicate NPC name>, <sprite>, <dir>, <xs> or <ys> is not provided the value of the original NPC will be used.
The Unique name of the new duplicated NPC is returned on success. An empty string is returned on failure.
NOTE:
Duplicates will always have the same NPC variables as the original NPC.
Editing a NPC variable in a duplicate or the original NPC will change it for the others.
---------------------------------------
*duplicate_dynamic("<NPC name>"{,<character ID>});
This command will duplicate the NPC with the given <NPC name> near the attached player or the player with the given <character ID>.
The Unique name of the new duplicated NPC is returned on success. An empty string is returned on failure.
NOTE:
Duplicates will always have the same NPC variables as the original NPC.
Editing a NPC variable in a duplicate or the original NPC will change it for the others.

View File

@ -10271,6 +10271,11 @@ static const struct _battle_data {
{ "macro_detection_retry", &battle_config.macro_detection_retry, 3, 1, INT_MAX, },
{ "macro_detection_timeout", &battle_config.macro_detection_timeout, 60000, 0, INT_MAX, },
{ "feature.dynamicnpc_timeout", &battle_config.feature_dynamicnpc_timeout, 1000, 60000, INT_MAX, },
{ "feature.dynamicnpc_rangex", &battle_config.feature_dynamicnpc_rangex, 2, 0, INT_MAX, },
{ "feature.dynamicnpc_rangey", &battle_config.feature_dynamicnpc_rangey, 2, 0, INT_MAX, },
{ "feature.dynamicnpc_direction", &battle_config.feature_dynamicnpc_direction, 0, 0, 1, },
#include "../custom/battle_config_init.inc"
};

View File

@ -714,6 +714,11 @@ struct Battle_Config
int macro_detection_retry;
int macro_detection_timeout;
int feature_dynamicnpc_timeout;
int feature_dynamicnpc_rangex;
int feature_dynamicnpc_rangey;
int feature_dynamicnpc_direction;
#include "../custom/battle_config_struct.inc"
};

View File

@ -448,6 +448,11 @@ static int clif_send_sub(struct block_list *bl, va_list ap)
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.data[SC_INTRAVISION] && battle_check_target(src_bl,&sd->bl,BCT_ENEMY) > 0)
@ -4954,6 +4959,11 @@ void clif_getareachar_unit( struct map_session_data* sd,struct block_list *bl ){
if (clif_npc_mayapurple(bl))
return;
if( bl->type == BL_NPC && npc_is_hidden_dynamicnpc( *( (struct npc_data*)bl ), *sd ) ){
// Do not send anything
return;
}
ud = unit_bl2ud(bl);
if( ud && ud->walktimer != INVALID_TIMER ){
@ -25087,21 +25097,21 @@ void packetdb_readdb(){
*
*------------------------------------------*/
void do_init_clif(void) {
const char* colors[COLOR_MAX] = {
"0x00FF00",
"0xFF0000",
"0xFFFFFF",
"0xFFFF00",
"0x00FFFF",
"0xB5FFB5",
const int colors[COLOR_MAX] = {
0x00FF00, // COLOR_DEFAULT
0xFF0000, // COLOR_RED
0xFFFFFF, // COLOR_WHITE
0xFFFF00, // COLOR_YELLOW
0x00FFFF, // COLOR_CYAN
0xB5FFB5, // COLOR_LIGHT_GREEN
0xFFFF63, // COLOR_LIGHT_YELLOW
};
int i;
/**
* Setup Color Table (saves unnecessary load of strtoul on every call)
**/
for(i = 0; i < COLOR_MAX; i++) {
color_table[i] = strtoul(colors[i],NULL,0);
color_table[i] = (color_table[i] & 0x0000FF) << 16 | (color_table[i] & 0x00FF00) | (color_table[i] & 0xFF0000) >> 16;//RGB to BGR
for( int i = 0; i < COLOR_MAX; i++ ){
color_table[i] = ( colors[i] & 0x0000FF ) << 16 | ( colors[i] & 0x00FF00 ) | ( colors[i] & 0xFF0000 ) >> 16; //RGB to BGR
}
packetdb_readdb();

View File

@ -556,6 +556,7 @@ enum clif_messages : uint16_t {
MSG_ATTENDANCE_DISABLED = 0xd92,
// Unofficial names
C_DYNAMICNPC_TWICE = 0xa47, /// <"Is already in service. Please try again in a few minutes."
C_ITEM_EQUIP_SWITCH = 0xbc7,
C_ITEM_NOEQUIP = 0x174, /// <"You can't put this item on."
C_ENCHANT_OVERWEIGHT = 0xEFD,
@ -961,6 +962,7 @@ void clif_equipcheckbox(struct map_session_data* sd);
void clif_msg(struct map_session_data* sd, unsigned short id);
void clif_msg_value(struct map_session_data* sd, unsigned short id, int value);
void clif_msg_skill(struct map_session_data* sd, uint16 skill_id, int msg_id);
void clif_msg_color( struct map_session_data* sd, uint16 msg_id, uint32 color );
//quest system [Kevin] [Inkfish]
void clif_quest_send_list(struct map_session_data * sd);
@ -1126,6 +1128,7 @@ enum clif_colors {
COLOR_YELLOW,
COLOR_CYAN,
COLOR_LIGHT_GREEN,
COLOR_LIGHT_YELLOW,
COLOR_MAX
};
extern unsigned long color_table[COLOR_MAX];

View File

@ -571,7 +571,7 @@ int map_count_oncell(int16 m, int16 x, int16 y, int type, int flag)
if(bl->x == x && bl->y == y && bl->type&type) {
if (bl->type == BL_NPC) { // Don't count hidden or invisible npc. Cloaked npc are counted
npc_data *nd = BL_CAST(BL_NPC, bl);
if (nd && (nd->bl.m < 0 || nd->sc.option && nd->sc.option&(OPTION_HIDE|OPTION_INVISIBLE)))
if (nd->bl.m < 0 || nd->sc.option&(OPTION_HIDE|OPTION_INVISIBLE) || nd->dynamicnpc.owner_char_id != 0)
continue;
}
if(flag&1) {

View File

@ -70,6 +70,8 @@ static void npc_market_fromsql(void);
#define npc_market_clearfromsql(exname) (npc_market_delfromsql_((exname), 0, true))
#endif
TIMER_FUNC(npc_dynamicnpc_removal_timer);
/// Returns a new npc id that isn't being used in id_db.
/// Fatal error if nothing is available.
int npc_get_new_npc_id(void) {
@ -864,6 +866,10 @@ int npc_isnear_sub(struct block_list* bl, va_list args) {
if (nd->sc.option & (OPTION_HIDE|OPTION_INVISIBLE))
return 0;
if( nd->dynamicnpc.owner_char_id != 0 ){
return 0;
}
int skill_id = va_arg(args, int);
if (skill_id > 0) { //If skill_id > 0 that means is used for INF2_DISABLENEARNPC [Cydh]
@ -954,6 +960,11 @@ bool npc_is_cloaked(struct npc_data* nd, struct map_session_data* sd) {
return npc_cloaked;
}
bool npc_is_hidden_dynamicnpc( struct npc_data& nd, struct map_session_data& tsd ){
// If the NPC is dynamic and the target character is not the owner of the dynamic NPC
return nd.dynamicnpc.owner_char_id != 0 && nd.dynamicnpc.owner_char_id != tsd.status.char_id;
}
static int npc_cloaked_sub(struct block_list *bl, va_list ap)
{
struct map_session_data* sd;
@ -1839,6 +1850,10 @@ int npc_touch_areanpc(struct map_session_data* sd, int16 m, int16 x, int16 y, st
if (npc_is_cloaked(nd, sd))
return 1;
if( npc_is_hidden_dynamicnpc( *nd, *sd ) ){
return 1;
}
int xs = -1, ys = -1;
switch(nd->subtype) {
case NPCTYPE_WARP:
@ -1939,6 +1954,10 @@ int npc_touch_areanpc2(struct mob_data *md)
if( mapdata->npc[i]->sc.option&(OPTION_INVISIBLE|OPTION_CLOAK) )
continue;
if( mapdata->npc[i]->dynamicnpc.owner_char_id != 0 ){
continue;
}
switch( mapdata->npc[i]->subtype )
{
case NPCTYPE_WARP:
@ -2032,6 +2051,10 @@ int npc_check_areanpc(int flag, int16 m, int16 x, int16 y, int16 range)
if (mapdata->npc[i]->sc.option&OPTION_INVISIBLE)
continue;
if( mapdata->npc[i]->dynamicnpc.owner_char_id != 0 ){
continue;
}
switch(mapdata->npc[i]->subtype)
{
case NPCTYPE_WARP:
@ -2149,11 +2172,19 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd)
if (nd->class_ < 0 || nd->sc.option&(OPTION_INVISIBLE|OPTION_HIDE))
return 1;
if( npc_is_hidden_dynamicnpc( *nd, *sd ) ){
return 1;
}
if (sd->state.block_action & PCBLOCK_NPCCLICK) {
clif_msg(sd, WORK_IN_PROGRESS);
return 1;
}
if( nd->dynamicnpc.owner_char_id != 0 ){
nd->dynamicnpc.last_interaction = gettick();
}
switch(nd->subtype) {
case NPCTYPE_SHOP:
clif_npcbuysell(sd,nd->bl.id);
@ -2207,6 +2238,7 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd)
*------------------------------------------*/
bool npc_scriptcont(struct map_session_data* sd, int id, bool closing){
struct block_list *target = map_id2bl(id);
struct npc_data* nd = BL_CAST( BL_NPC, target );
nullpo_retr(true, sd);
@ -2217,7 +2249,6 @@ bool npc_scriptcont(struct map_session_data* sd, int id, bool closing){
if( id != sd->npc_id ){
TBL_NPC* nd_sd = (TBL_NPC*)map_id2bl(sd->npc_id);
TBL_NPC* nd = BL_CAST(BL_NPC, target);
ShowDebug("npc_scriptcont: %s (sd->npc_id=%d) is not %s (id=%d).\n",
nd_sd?(char*)nd_sd->name:"'Unknown NPC'", (int)sd->npc_id,
@ -2236,6 +2267,10 @@ bool npc_scriptcont(struct map_session_data* sd, int id, bool closing){
sd->npc_idle_tick = gettick(); //Update the last NPC iteration
#endif
if( nd != nullptr && nd->dynamicnpc.owner_char_id != 0 ){
nd->dynamicnpc.last_interaction = gettick();
}
/**
* WPE can get to this point with a progressbar; we deny it.
**/
@ -2312,6 +2347,10 @@ int npc_buysellsel(struct map_session_data* sd, int id, int type)
if (nd->sc.option & OPTION_INVISIBLE) // can't buy if npc is not visible (hack?)
return 1;
if( npc_is_hidden_dynamicnpc( *nd, *sd ) ){
return 1;
}
sd->npc_shopid = id;
if (type == 0) {
@ -3477,6 +3516,20 @@ int npc_unload(struct npc_data* nd, bool single) {
nd->qi_data.clear();
script_stop_sleeptimers(nd->bl.id);
if( nd->dynamicnpc.removal_tid != INVALID_TIMER ){
delete_timer( nd->dynamicnpc.removal_tid, npc_dynamicnpc_removal_timer );
nd->dynamicnpc.removal_tid = INVALID_TIMER;
}
if( nd->dynamicnpc.owner_char_id != 0 ){
struct map_session_data* owner = map_charid2sd( nd->dynamicnpc.owner_char_id );
if( owner != nullptr ){
owner->npc_id_dynamic = 0;
}
}
aFree(nd);
return 0;
@ -3719,6 +3772,9 @@ struct npc_data *npc_create_npc(int16 m, int16 x, int16 y){
nd->sc_display_count = 0;
nd->progressbar.timeout = 0;
nd->vd = npc_viewdb[0]; // Default to JT_INVISIBLE
nd->dynamicnpc.owner_char_id = 0;
nd->dynamicnpc.last_interaction = 0;
nd->dynamicnpc.removal_tid = INVALID_TIMER;
#ifdef MAP_GENERATOR
nd->navi.pos = {m, x, y};
@ -4388,8 +4444,7 @@ static const char* npc_parse_script(char* w1, char* w2, char* w3, char* w4, cons
/// shop/cashshop/npc: <map name>,<x>,<y>,<facing>%TAB%duplicate(<name of target>)%TAB%<NPC Name>%TAB%<sprite id>
/// npc: -%TAB%duplicate(<name of target>)%TAB%<NPC Name>%TAB%<sprite id>,<triggerX>,<triggerY>
/// npc: <map name>,<x>,<y>,<facing>%TAB%duplicate(<name of target>)%TAB%<NPC Name>%TAB%<sprite id>,<triggerX>,<triggerY>
const char* npc_parse_duplicate(char* w1, char* w2, char* w3, char* w4, const char* start, const char* buffer, const char* filepath)
{
const char* npc_parse_duplicate( char* w1, char* w2, char* w3, char* w4, const char* start, const char* buffer, const char* filepath, struct map_session_data* owner = nullptr ){
short x, y, m, xs = -1, ys = -1;
int16 dir;
char srcname[128];
@ -4455,6 +4510,12 @@ const char* npc_parse_duplicate(char* w1, char* w2, char* w3, char* w4, const ch
nd->src_id = src_id;
nd->bl.type = BL_NPC;
nd->subtype = (enum npc_subtype)type;
if( owner != nullptr ){
nd->dynamicnpc.owner_char_id = owner->status.char_id;
owner->npc_id_dynamic = nd->bl.id;
}
switch( type ) {
case NPCTYPE_SCRIPT:
++npc_script;
@ -5657,13 +5718,13 @@ int npc_script_event(struct map_session_data* sd, enum npce_event type){
* dir: Facing direction of duplicate NPC
* Returns duplicate NPC data on success
*/
npc_data* npc_duplicate_npc( npc_data* nd, char name[NPC_NAME_LENGTH + 1], int16 mapid, int16 x, int16 y, int class_, uint8 dir, int16 xs, int16 ys ){
npc_data* npc_duplicate_npc( npc_data& nd, char name[NPC_NAME_LENGTH + 1], int16 mapid, int16 x, int16 y, int class_, uint8 dir, int16 xs, int16 ys, struct map_session_data* owner ){
static char w1[128], w2[128], w3[128], w4[128];
const char* stat_buf = "- call from duplicate subsystem -\n";
char exname[NPC_NAME_LENGTH + 1];
snprintf(w1, sizeof(w1), "%s,%d,%d,%d", map_getmapdata(mapid)->name, x, y, dir);
snprintf(w2, sizeof(w2), "duplicate(%s)", nd->exname);
snprintf(w2, sizeof(w2), "duplicate(%s)", nd.exname);
//Making sure the generated name is not used for another npc.
int i = 0;
@ -5681,7 +5742,7 @@ npc_data* npc_duplicate_npc( npc_data* nd, char name[NPC_NAME_LENGTH + 1], int16
snprintf( w4, sizeof( w4 ), "%d", class_ );
}
npc_parse_duplicate(w1, w2, w3, w4, stat_buf, stat_buf, "DUPLICATE");//DUPLICATE means nothing for now.
npc_parse_duplicate( w1, w2, w3, w4, stat_buf, stat_buf, "DUPLICATE", owner ); //DUPLICATE means nothing for now.
npc_data* dnd = npc_name2id( exname );
@ -5700,6 +5761,81 @@ npc_data* npc_duplicate_npc( npc_data* nd, char name[NPC_NAME_LENGTH + 1], int16
return dnd;
}
TIMER_FUNC(npc_dynamicnpc_removal_timer){
struct npc_data* nd = map_id2nd( id );
if( nd == nullptr ){
return 0;
}
nd->dynamicnpc.removal_tid = INVALID_TIMER;
struct map_session_data* sd = map_charid2sd( nd->dynamicnpc.owner_char_id );
if( sd != nullptr ){
// Still talking to the NPC
// TODO: are there other fields to check?
if( sd->npc_id == nd->bl.id || sd->npc_shopid == nd->bl.id ){
// Retry later
nd->dynamicnpc.last_interaction = gettick();
nd->dynamicnpc.removal_tid = add_timer( nd->dynamicnpc.last_interaction + battle_config.feature_dynamicnpc_timeout, npc_dynamicnpc_removal_timer, nd->bl.id, NULL );
return 0;
}
// Last interaction is not long enough in the past
if( DIFF_TICK( gettick(), nd->dynamicnpc.last_interaction ) < battle_config.feature_dynamicnpc_timeout ){
nd->dynamicnpc.removal_tid = add_timer( nd->dynamicnpc.last_interaction + DIFF_TICK( gettick(), nd->dynamicnpc.last_interaction ), npc_dynamicnpc_removal_timer, nd->bl.id, NULL );
return 0;
}
sd->npc_id_dynamic = 0;
}
// Delete the NPC
npc_unload( nd, true );
// Update NPC event database
npc_read_event_script();
return 0;
}
struct npc_data* npc_duplicate_npc_for_player( struct npc_data& nd, struct map_session_data& sd ){
if( sd.npc_id_dynamic != 0 ){
clif_msg_color( &sd, C_DYNAMICNPC_TWICE, color_table[COLOR_LIGHT_YELLOW] );
return nullptr;
}
// TODO: check maps that might forbid usage? maybe create mapflag?
int16 new_x, new_y;
if( !map_search_freecell( &sd.bl, 0, &new_x, &new_y, battle_config.feature_dynamicnpc_rangex, battle_config.feature_dynamicnpc_rangey, 0 ) ){
ShowError( "npc_duplicate_npc_for_player: Unable to find a free cell to duplicate NPC \"%s\" for player \"%s\"(AID: %u, CID: %u)\n", nd.exname, sd.status.name, sd.status.account_id, sd.status.char_id );
return nullptr;
}
int8 dir;
if( battle_config.feature_dynamicnpc_direction ){
// Let the NPC look to the player
dir = map_calc_dir_xy( new_x, new_y, sd.bl.x, sd.bl.y, DIR_CENTER );
}else{
// Use original NPCs direction
dir = nd.ud.dir;
}
struct npc_data* dnd = npc_duplicate_npc( nd, nd.name, sd.bl.m, new_x, new_y, nd.class_, dir, nd.u.scr.xs, nd.u.scr.ys, &sd );
if( dnd == nullptr ){
return nullptr;
}
dnd->dynamicnpc.last_interaction = gettick();
dnd->dynamicnpc.removal_tid = add_timer( dnd->dynamicnpc.last_interaction + battle_config.feature_dynamicnpc_timeout, npc_dynamicnpc_removal_timer, dnd->bl.id, NULL );
return dnd;
}
const char *npc_get_script_event_name(int npce_index)
{
switch (npce_index) {
@ -6034,11 +6170,10 @@ void do_init_npc(void){
add_timer_func_list(npc_event_do_clock,"npc_event_do_clock");
add_timer_func_list(npc_timerevent,"npc_timerevent");
add_timer_func_list( npc_dynamicnpc_removal_timer, "npc_dynamicnpc_removal_timer" );
// Init dummy NPC
fake_nd = (struct npc_data *)aCalloc(1,sizeof(struct npc_data));
fake_nd->bl.m = -1;
fake_nd->bl.id = npc_get_new_npc_id();
fake_nd = npc_create_npc( -1, 0, 0 );
fake_nd->class_ = JT_FAKENPC;
fake_nd->speed = 200;
strcpy(fake_nd->name,"FAKE_NPC");

View File

@ -208,6 +208,12 @@ struct npc_data {
unsigned long color;
} progressbar;
struct{
uint32 owner_char_id;
t_tick last_interaction;
int removal_tid;
} dynamicnpc;
#ifdef MAP_GENERATOR
struct navi_link navi; // for warps and the src of npcs
std::vector<navi_link> links; // for extra links, like warper npc
@ -1494,12 +1500,14 @@ void npc_parse_mob2(struct spawn_data* mob);
struct npc_data* npc_add_warp(char* name, short from_mapid, short from_x, short from_y, short xs, short ys, unsigned short to_mapindex, short to_x, short to_y);
int npc_globalmessage(const char* name,const char* mes);
const char *npc_get_script_event_name(int npce_index);
npc_data* npc_duplicate_npc( npc_data* nd, char name[NPC_NAME_LENGTH + 1], int16 mapid, int16 x, int16 y, int class_, uint8 dir, int16 xs, int16 ys );
npc_data* npc_duplicate_npc( npc_data& nd, char name[NPC_NAME_LENGTH + 1], int16 mapid, int16 x, int16 y, int class_, uint8 dir, int16 xs, int16 ys, struct map_session_data* owner = nullptr );
struct npc_data* npc_duplicate_npc_for_player( struct npc_data& nd, struct map_session_data& sd );
void npc_setcells(struct npc_data* nd);
void npc_unsetcells(struct npc_data* nd);
bool npc_movenpc(struct npc_data* nd, int16 x, int16 y);
bool npc_is_cloaked(struct npc_data* nd, struct map_session_data* sd);
bool npc_is_hidden_dynamicnpc( struct npc_data& nd, struct map_session_data& tsd );
bool npc_enable_target(npc_data& nd, uint32 char_id, e_npcv_status flag);
#define npc_enable(nd, flag) npc_enable_target(nd, 0, flag)
void npc_setdisplayname(struct npc_data* nd, const char* newname);

View File

@ -491,6 +491,7 @@ struct map_session_data {
unsigned char head_dir; //0: Look forward. 1: Look right, 2: Look left.
t_tick client_tick;
int npc_id,npc_shopid; //for script follow scriptoid; ,npcid
int npc_id_dynamic;
std::vector<int> areanpc, npc_ontouch_; ///< Array of OnTouch and OnTouch_ NPC ID
int npc_item_flag; //Marks the npc_id with which you can use items during interactions with said npc (see script command enable_itemuse)
int npc_menu; // internal variable, used in npc menu handling

View File

@ -25193,7 +25193,40 @@ BUILDIN_FUNC(duplicate)
ys = nd->u.scr.ys;
}
npc_data* dnd = npc_duplicate_npc( nd, name, mapid, x, y, class_, dir, xs, ys );
npc_data* dnd = npc_duplicate_npc( *nd, name, mapid, x, y, class_, dir, xs, ys );
if( dnd == nullptr ){
script_pushstrcopy( st, "" );
return SCRIPT_CMD_FAILURE;
}else{
script_pushstrcopy( st, dnd->exname );
return SCRIPT_CMD_SUCCESS;
}
}
/**
* Duplicate a NPC for a player.
* Return the duplicate Unique name on success or empty string on failure.
* duplicate_dynamic("<NPC name>"{,<character ID>});
*/
BUILDIN_FUNC(duplicate_dynamic){
const char* old_npcname = script_getstr( st, 2 );
struct npc_data* nd = npc_name2id( old_npcname );
if( nd == nullptr ){
ShowError( "buildin_duplicate_dynamic: No such NPC '%s'.\n", old_npcname );
script_pushstrcopy( st, "" );
return SCRIPT_CMD_FAILURE;
}
struct map_session_data* sd;
if( !script_charid2sd( 3, sd ) ){
script_pushstrcopy( st, "" );
return SCRIPT_CMD_FAILURE;
}
struct npc_data* dnd = npc_duplicate_npc_for_player( *nd, *sd );
if( dnd == nullptr ){
script_pushstrcopy( st, "" );
@ -27427,6 +27460,7 @@ struct script_function buildin_func[] = {
BUILDIN_DEF(openstorage2,"ii?"),
BUILDIN_DEF(unloadnpc, "s"),
BUILDIN_DEF(duplicate, "ssii?????"),
BUILDIN_DEF(duplicate_dynamic, "s?"),
// WoE TE
BUILDIN_DEF(agitstart3,""),

View File

@ -3426,6 +3426,19 @@ int unit_free(struct block_list *bl, clr_type clrtype)
sd->npc_id = 0;
}
if( sd->npc_id_dynamic != 0 ){
struct npc_data* nd = map_id2nd( sd->npc_id_dynamic );
if( nd != nullptr ){
// Delete the NPC
npc_unload( nd, true );
// Update NPC event database
npc_read_event_script();
}
sd->npc_id_dynamic = 0;
}
sd->combos.clear();
if( sd->sc_display_count ) { /* [Ind] */