Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Dynamic NPC's #3138

Merged
merged 1 commit into from Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions conf/map/battle/misc.conf
Expand Up @@ -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
39 changes: 39 additions & 0 deletions 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;
}
11 changes: 11 additions & 0 deletions doc/script_commands.txt
Expand Up @@ -10989,3 +10989,14 @@ Works from clients: main 20220504.

Opens Grade Enchant user interface for the player
returns true on success and false on failure

---------------------------------------
*calldynamicnpc("<npc name>");

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
2 changes: 2 additions & 0 deletions src/map/battle.c
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/map/battle.h
Expand Up @@ -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 */
Expand Down
10 changes: 10 additions & 0 deletions src/map/clif.c
Expand Up @@ -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;
Expand Down Expand Up @@ -4984,6 +4991,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 )
Expand Down
2 changes: 1 addition & 1 deletion src/map/map.c
Expand Up @@ -491,7 +491,7 @@ static int map_count_oncell(int16 m, int16 x, int16 y, int type, int flag)
continue;
if (bl->type == BL_NPC) {
const struct npc_data *nd = BL_UCCAST(BL_NPC, bl);
if (nd->class_ == FAKE_NPC || nd->class_ == HIDDEN_WARP_CLASS)
if (nd->class_ == FAKE_NPC || nd->class_ == HIDDEN_WARP_CLASS || nd->dyn.isdynamic)
continue;
}
}
Expand Down
65 changes: 61 additions & 4 deletions src/map/npc.c
Expand Up @@ -157,6 +157,9 @@ static int npc_isnear_sub(struct block_list *bl, va_list args)
if( battle_config.vendchat_near_hiddennpc && ( nd->class_ == FAKE_NPC || nd->class_ == HIDDEN_WARP_CLASS ) )
return 0;

if (nd->dyn.isdynamic)
return 0;

return 1;
}

Expand Down Expand Up @@ -226,6 +229,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)
Expand Down Expand Up @@ -1018,6 +1024,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:
Expand Down Expand Up @@ -1132,6 +1142,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:
Expand Down Expand Up @@ -1217,6 +1229,8 @@ static int npc_check_areanpc(int flag, int16 m, int16 x, int16 y, int16 range)
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:
Expand Down Expand Up @@ -1364,6 +1378,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);
Expand Down Expand Up @@ -1439,6 +1460,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)
Expand Down Expand Up @@ -1483,6 +1506,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;
}
Expand Down Expand Up @@ -3471,6 +3497,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;
}

Expand Down Expand Up @@ -4462,7 +4493,6 @@ static int npc_do_atcmd_event(struct map_session_data *sd, const char *command,
struct npc_data *nd;
struct script_state *st;
int i = 0, nargs = 0;
size_t len;

nullpo_ret(sd);
nullpo_ret(message);
Expand All @@ -4483,16 +4513,16 @@ 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) != 0 || ev->nd->dyn.isdynamic) { // Disabled npc, shouldn't trigger event.
npc->event_dequeue(sd);
return 2;
}

st = script->alloc_state(ev->nd->u.scr.script, ev->pos, sd->bl.id, ev->nd->bl.id);
script->setd_sub(st, NULL, ".@atcmd_command$", 0, command, NULL);

len = strlen(message);
if (len) {
int len = (int)strlen(message);
if (len > 0) {
char *temp, *p;
p = temp = aStrdup(message);
// Sanity check - Skip leading spaces (shouldn't happen)
Expand Down Expand Up @@ -5911,6 +5941,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
*------------------------------------------*/
Expand Down Expand Up @@ -6140,4 +6195,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;
}
11 changes: 11 additions & 0 deletions src/map/npc.h
Expand Up @@ -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
};

Expand Down Expand Up @@ -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
Expand Down