diff --git a/conf/map/battle/misc.conf b/conf/map/battle/misc.conf index 1e739c8a1eb..bcb508f1e5a 100644 --- a/conf/map/battle/misc.conf +++ b/conf/map/battle/misc.conf @@ -175,3 +175,11 @@ case_sensitive_aegisnames: true // Aegis: 15 // eAthena: 1 search_freecell_map_margin: 15 + +// The inactivity duration before a dynamic npc will despawn +// Aegis: 60 seconds +dynamic_npc_timeout: 60000 + +// The range for dynamic npc location search +// Aegis: 2 +dynamic_npc_range: 2 diff --git a/doc/sample/npc_calldynamicnpc.txt b/doc/sample/npc_calldynamicnpc.txt new file mode 100644 index 00000000000..243fc6ea9be --- /dev/null +++ b/doc/sample/npc_calldynamicnpc.txt @@ -0,0 +1,39 @@ +//===== Hercules Script ====================================== +//= Sample: Dynamic NPC Call +//===== By: ================================================== +//= Hercules Dev Team +//===== Current Version: ===================================== +//= 20220501 +//===== Description: ========================================= +//= Contains an example of calldynamicnpc +//============================================================ + +prontera,155,284,4 script Teleport Service 4_M_DRZONDA01,{ + switch (select("GlastHeim", "Amatsu")) { + case 1: calldynamicnpc("GlastHeim Teleporter"); break; + case 2: calldynamicnpc("Amatsu Teleporter"); break; + } + close(); +} + +// The source npc must have a real location in order to preserve +// the view data, if you don't assign a map it will be treated as FAKE_NPC +prontera,0,0,0 script GlastHeim Teleporter PORTAL,{ + warp("glast_01", 200, 269); + end; +OnDynamicNpcInit: + specialeffect(EF_ANGEL2, SELF, playerattached()); + end; +OnInit: + // Disable the source npc just in case + disablenpc(strnpcinfo(NPC_NAME)); + end; +} + +prontera,0,0,0 script Amatsu Teleporter PORTAL,{ + warp("amatsu", 197, 79); + end; +OnInit: + disablenpc(strnpcinfo(NPC_NAME)); + end; +} diff --git a/doc/script_commands.txt b/doc/script_commands.txt index 21e6b53aed5..95dc60658fa 100644 --- a/doc/script_commands.txt +++ b/doc/script_commands.txt @@ -10924,3 +10924,14 @@ Works from clients: main 20210203, re 20211103 Opens Grade Enchant user interface for the player returns true on success and false on failure + +--------------------------------------- +*calldynamicnpc(""); + +Creates a dynamic npc copy in range of the player with certain criteria: + - Only the owner of the npc can see and interact with the npc + - The npc will despawn after a certain period of inactivity + +a dynamic npc cannot be created from a FAKE_NPC source +and it does not run OnInit event when created but rather uses OnDynamicNpcInit. +returns true on success and false on failure diff --git a/src/map/battle.c b/src/map/battle.c index 57a4428301b..fcc88fef4ea 100644 --- a/src/map/battle.c +++ b/src/map/battle.c @@ -7856,6 +7856,8 @@ static const struct battle_data { { "roulette_silver_step", &battle_config.roulette_silver_step, 10, 1, INT_MAX, }, { "roulette_bronze_step", &battle_config.roulette_bronze_step, 1, 1, INT_MAX, }, { "features/grader_max_used", &battle_config.grader_max_used, 0, 0, MAX_ITEM_GRADE, }, + { "dynamic_npc_timeout", &battle_config.dynamic_npc_timeout, 0, 0, INT_MAX, }, + { "dynamic_npc_range", &battle_config.dynamic_npc_range, 0, 0, INT_MAX, }, }; static bool battle_set_value_sub(int index, int value) diff --git a/src/map/battle.h b/src/map/battle.h index 814a217fadc..99015356cd0 100644 --- a/src/map/battle.h +++ b/src/map/battle.h @@ -637,6 +637,8 @@ struct Battle_Config { int roulette_silver_step; int roulette_bronze_step; int grader_max_used; + int dynamic_npc_timeout; + int dynamic_npc_range; }; /* criteria for battle_config.idletime_criteria */ diff --git a/src/map/clif.c b/src/map/clif.c index ceff9e49b41..a9a2deea847 100644 --- a/src/map/clif.c +++ b/src/map/clif.c @@ -389,6 +389,13 @@ static int clif_send_sub(struct block_list *bl, va_list ap) #endif } + // Supress sending area packets for dynamic npcs + if (src_bl->type == BL_NPC) { + const struct npc_data *nd = BL_UCCAST(BL_NPC, src_bl); + if (nd->dyn.isdynamic && nd->dyn.owner_id != sd->status.char_id) + return 0; + } + /* unless visible, hold it here */ if( clif->ally_only && !sd->sc.data[SC_CLAIRVOYANCE] && !sd->special_state.intravision && battle->check_target( src_bl, &sd->bl, BCT_ENEMY ) > 0 ) return 0; @@ -4937,6 +4944,9 @@ static void clif_getareachar_unit(struct map_session_data *sd, struct block_list struct npc_data *nd = BL_UCAST(BL_NPC, bl); if (nd->chat_id == 0 && (nd->option&OPTION_INVISIBLE)) return; + // Dynamic npcs are hidden from non owner characters + if (nd->dyn.isdynamic && nd->dyn.owner_id != sd->status.char_id) + return; } if ( ( ud = unit->bl2ud(bl) ) && ud->walktimer != INVALID_TIMER ) diff --git a/src/map/npc.c b/src/map/npc.c index 76c1582526f..3d7e0e48b8f 100644 --- a/src/map/npc.c +++ b/src/map/npc.c @@ -226,6 +226,9 @@ static int npc_enable_sub(struct block_list *bl, va_list ap) if (nd->option&OPTION_INVISIBLE) return 1; + if (nd->dyn.isdynamic && nd->dyn.owner_id != sd->status.char_id) + return 1; + if( npc->ontouch_event(sd,nd) > 0 && npc->ontouch2_event(sd,nd) > 0 ) { // failed to run OnTouch event, so just click the npc if (sd->npc_id != 0) @@ -1018,6 +1021,10 @@ static int npc_touch_areanpc(struct map_session_data *sd, int16 m, int16 x, int1 f=0; // a npc was found, but it is disabled; don't print warning continue; } + if (map->list[m].npc[i]->dyn.isdynamic && map->list[m].npc[i]->dyn.owner_id != sd->status.char_id) { + f = 0; + continue; + } switch(map->list[m].npc[i]->subtype) { case WARP: @@ -1132,6 +1139,8 @@ static int npc_touch_areanpc2(struct mob_data *md) for( i = 0; i < map->list[m].npc_num; i++ ) { if( map->list[m].npc[i]->option&OPTION_INVISIBLE ) continue; + if (map->list[m].npc[i]->dyn.isdynamic) + continue; switch( map->list[m].npc[i]->subtype ) { case WARP: @@ -1217,6 +1226,8 @@ static int npc_check_areanpc(int flag, int16 m, int16 x, int16 y, int16 range) for(i=0;ilist[m].npc_num;i++) { if (map->list[m].npc[i]->option&OPTION_INVISIBLE) continue; + if (map->list[m].npc[i]->dyn.isdynamic) + continue; switch(map->list[m].npc[i]->subtype) { case WARP: @@ -1364,6 +1375,13 @@ static int npc_click(struct map_session_data *sd, struct npc_data *nd) if (nd->class_ < 0 || nd->option&(OPTION_INVISIBLE|OPTION_HIDE)) return 1; + // Dynamic npcs only triggerable by the owner + if (nd->dyn.isdynamic && nd->dyn.owner_id != sd->status.char_id) + return 1; + + // Update the interaction tick + npc->update_interaction_tick(nd); + switch(nd->subtype) { case SHOP: clif->npcbuysell(sd,nd->bl.id); @@ -1439,6 +1457,8 @@ static int npc_scriptcont(struct map_session_data *sd, int id, bool closing) #ifdef SECURE_NPCTIMEOUT sd->npc_idle_tick = timer->gettick(); /// Update the last NPC iteration. #endif + // Update the interaction tick + npc->update_interaction_tick(BL_CAST(BL_NPC, target)); /// WPE can get to this point with a progressbar; we deny it. if (sd->progressbar.npc_id != 0 && DIFF_TICK(sd->progressbar.timeout, timer->gettick()) > 0) @@ -1483,6 +1503,9 @@ static int npc_buysellsel(struct map_session_data *sd, int id, int type) if (nd->option & OPTION_INVISIBLE) // can't buy if npc is not visible (hack?) return 1; + if (nd->dyn.isdynamic && nd->dyn.owner_id != sd->status.char_id) + return 1; + if( nd->class_ < 0 && !sd->state.callshop ) {// not called through a script and is not a visible NPC so an invalid call return 1; } @@ -3471,6 +3494,11 @@ static struct npc_data *npc_create_npc(enum npc_subtype subtype, int m, int x, i nd->vd = npc_viewdb[0]; // Copy INVISIBLE_CLASS view data. Actual view data is set by npc->add_to_location() later. VECTOR_INIT(nd->qi_data); + nd->dyn.isdynamic = false; + nd->dyn.owner_id = 0; + nd->dyn.despawn_timer = INVALID_TIMER; + nd->dyn.last_interaction_tick = timer->gettick(); + return nd; } @@ -4483,7 +4511,7 @@ static int npc_do_atcmd_event(struct map_session_data *sd, const char *command, return 1; } - if( ev->nd->option&OPTION_INVISIBLE ) { // Disabled npc, shouldn't trigger event. + if (ev->nd->option & OPTION_INVISIBLE || ev->nd->dyn.isdynamic) { // Disabled npc, shouldn't trigger event. npc->event_dequeue(sd); return 2; } @@ -5911,6 +5939,31 @@ static void npc_questinfo_clear(struct npc_data *nd) VECTOR_CLEAR(nd->qi_data); } +static int npc_dynamic_npc_despawn(int tid, int64 tick, int id, intptr_t data) +{ + struct npc_data *nd = map->id2nd(id); + + if (nd == NULL || nd->dyn.despawn_timer != tid) + return 0; + + if (DIFF_TICK(tick, nd->dyn.last_interaction_tick) >= data) { + nd->dyn.despawn_timer = INVALID_TIMER; + npc->unload(nd, true, false); + return 0; + } + + int64 next_tick = battle->bc->dynamic_npc_timeout - DIFF_TICK(tick, nd->dyn.last_interaction_tick); + nd->dyn.despawn_timer = timer->add(tick + next_tick, npc->dynamic_npc_despawn, nd->bl.id, (intptr_t)next_tick); + return 0; +} + +static void npc_update_interaction_tick(struct npc_data *nd) +{ + nullpo_retv(nd); + + nd->dyn.last_interaction_tick = timer->gettick(); +} + /*========================================== * npc initialization *------------------------------------------*/ @@ -6140,4 +6193,6 @@ void npc_defaults(void) npc->refresh = npc_refresh; npc->questinfo_clear = npc_questinfo_clear; npc->process_files = npc_process_files; + npc->dynamic_npc_despawn = npc_dynamic_npc_despawn; + npc->update_interaction_tick = npc_update_interaction_tick; } diff --git a/src/map/npc.h b/src/map/npc.h index c09cbaefab6..03d400edb78 100644 --- a/src/map/npc.h +++ b/src/map/npc.h @@ -147,6 +147,14 @@ struct npc_data { } tomb; } u; VECTOR_DECL(struct questinfo) qi_data; + + struct { + bool isdynamic; + int owner_id; + int64 last_interaction_tick; + int despawn_timer; + } dyn; + struct hplugin_data_store *hdata; ///< HPM Plugin Data Store }; @@ -353,6 +361,9 @@ struct npc_interface { **/ int (*secure_timeout_timer) (int tid, int64 tick, int id, intptr_t data); void (*process_files) (int npc_min); + + int (*dynamic_npc_despawn) (int tid, int64 tick, int id, intptr_t data); + void (*update_interaction_tick) (struct npc_data *nd); }; #ifdef HERCULES_CORE diff --git a/src/map/script.c b/src/map/script.c index 33c0adf0c53..bb338345652 100644 --- a/src/map/script.c +++ b/src/map/script.c @@ -27664,6 +27664,98 @@ static BUILDIN(opengradeui) #endif } +static BUILDIN(calldynamicnpc) +{ + struct map_session_data *sd = script_rid2sd(st); + if (sd == NULL) { + ShowError("buildin_calldynamicnpc: No player attached.\n"); + script->reportfunc(st); + script->reportsrc(st); + script_pushint(st, 0); + return false; + } + + struct npc_data *snd = NULL; + if (script_isstringtype(st, 2)) { + snd = npc->name2id(script_getstr(st, 2)); + } else { + snd = map->id2nd(script_getnum(st, 2)); + } + + if (snd == NULL) { + ShowError("buildin_calldynamicnpc: NPC not found.\n"); + script->reportfunc(st); + script->reportsrc(st); + script_pushint(st, 0); + return false; + } + + if (snd->class_ == FAKE_NPC) { + ShowError("buildin_calldynamicnpc: trying to create a dynamic npc using a FAKE_NPC.\n"); + script->reportfunc(st); + script->reportsrc(st); + script_pushint(st, 0); + return false; + } + + int16 x = 0; + int16 y = 0; + if (map->search_free_cell(&sd->bl, sd->bl.m, &x, &y, battle->bc->dynamic_npc_range, battle->bc->dynamic_npc_range, SFC_REACHABLE) != 0) { + ShowError("buildin_calldynamicnpc: Failed to find a spawn cell.\n"); + script->reportfunc(st); + script->reportsrc(st); + script_pushint(st, 0); + return false; + } + + // Generate a unique npc name and return in case it already existed + char newname[NAME_LENGTH]; + safesnprintf(newname, NAME_LENGTH, "dyn_%10d%10d", snd->bl.id, sd->status.char_id); + if (npc->name2id(newname) != NULL) { + script_pushint(st, 0); + return true; + } + + // Create the npc + struct npc_data *nd_target = npc->create_npc(snd->subtype, sd->bl.m, x, y, UNIT_DIR_SOUTH, snd->class_); + + // Copy the original npc name + safestrncpy(nd_target->name, snd->name, sizeof(nd_target->name)); + safestrncpy(nd_target->exname, newname, sizeof(nd_target->exname)); + + // Set the dynamic npc data + nd_target->dyn.isdynamic = true; + nd_target->dyn.owner_id = sd->status.char_id; + nd_target->dyn.despawn_timer = timer->add(timer->gettick() + battle->bc->dynamic_npc_timeout, + npc->dynamic_npc_despawn, nd_target->bl.id, (intptr_t)battle->bc->dynamic_npc_timeout); + + // Spawn the npc + int xs = -1, ys = -1; + switch (snd->subtype) { + case SCRIPT: + xs = snd->u.scr.xs; + ys = snd->u.scr.ys; + break; + case WARP: + xs = snd->u.warp.xs; + ys = snd->u.warp.ys; + break; + case CASHSHOP: + case SHOP: + case TOMB: + default: // Other types have no xs/ys + break; + } + npc->duplicate_sub(nd_target, snd, xs, ys, NPO_NONE); + char evname[EVENT_NAME_LENGTH]; + safesnprintf(evname, EVENT_NAME_LENGTH, "%s::OnDynamicNpcInit", nd_target->exname); + struct event_data *ev = strdb_get(npc->ev_db, evname); + if (ev != NULL) + script->run_npc(ev->nd->u.scr.script, ev->pos, sd->bl.id, ev->nd->bl.id); + script_pushint(st, 1); + return true; +} + /** * Adds a built-in script function. * @@ -28518,6 +28610,7 @@ static void script_parse_builtin(void) BUILDIN_DEF(getequipisenablegrade, "i"), BUILDIN_DEF(getequipgrade, "i"), BUILDIN_DEF(opengradeui, ""), + BUILDIN_DEF(calldynamicnpc, "v"), }; int i, len = ARRAYLENGTH(BUILDIN); RECREATE(script->buildin, char *, script->buildin_count + len); // Pre-alloc to speed up