Skip to content

Commit

Permalink
Implement Dynamic NPC's
Browse files Browse the repository at this point in the history
  • Loading branch information
Asheraf committed May 1, 2022
1 parent 9d0de90 commit bcfbb4e
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 1 deletion.
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 @@ -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("<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 @@ -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 )
Expand Down
57 changes: 56 additions & 1 deletion src/map/npc.c
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1217,6 +1226,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 +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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
*------------------------------------------*/
Expand Down Expand Up @@ -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;
}
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
93 changes: 93 additions & 0 deletions src/map/script.c
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bcfbb4e

Please sign in to comment.