Skip to content

Commit

Permalink
Merge pull request #3138 from Asheraf/dynamicnpc
Browse files Browse the repository at this point in the history
Implement Dynamic NPC's
  • Loading branch information
MishimaHaruna authored Jun 2, 2022
2 parents e28dad7 + 005d35f commit 981cff1
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 5 deletions.
8 changes: 8 additions & 0 deletions conf/map/battle/misc.conf
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 981cff1

Please sign in to comment.