Go Back   EQEmulator Home > EQEmulator Forums > Quests > Quests::Q&A

Quests::Q&A This is the quest support section

Reply
 
Thread Tools Display Modes
  #1  
Old 02-28-2004, 11:16 AM
smogo
Discordant
 
Join Date: Jan 2004
Location: 47
Posts: 339
Default The Lazy Perl Quest Writer

Here i describe plugin setup that help writting sound and flavoured behaviour for NPCs, with great efficiency. It relies on default.pl, quest globals and plugins.

*** edited ***

overview
for NPCs that have no specific quest, use global variables to assign them to categories (group by race, job, interest, guild). Each group has a shared behavior. Write this behaviour once (simple perl script) for each category, and then tie NPC to 1 or more categories. The plugin ensures events will be passed accordingly to the correct script(s). The same pattern can applied to a quested NPC, when the event has nothing to do with the quest.

howto
you find details of how it works below, but yet :
get the files there
extract to EQEMu directory. There is plugin.pl, quests/default.pl, and 3 plugins
Select a NPC in freporte with no quest, and set variable 'job' to 'soulbinder', 'race' to 'dwarf'
restart server, log in, and chat with your NPC



details
Ok, this is a bit complex, so i'll go step by step.

Quests scripts allow NPC to reacts to events, hence, the quests.
When no special scenario exists, the NPC gets the default.pl script, whether the global one, or the zone specific if one exists.
This shows two things :
1. there can be a default behaviour for all NPCs,
2. there is precedence of 1234.pl over zonesn/default.pl, then over default.pl

Now, imagine you would like to have your average NPC to react according to its race / class / deity / .. whatever. But you don't want to write a specific script for him/her (this would make thousands of scripts).
It would be nice, in the default.pl, to test a few variables and switch to the appropriate answer.

One example (default.pl):
Code:
sub EVENT_SAY{
  if($mname =~ /^Soulbinder/{

    if($text=~/Hail/i){quest::say("Greetings $name. When a hero of our world is slain  their soul returns to the place it was last bound and the body is reincarnated. As a member of the Order of Eternity  it is my duty to [bind your soul] to this location if that is your wish.");}
    if($text=~/bind my soul/i) {
      quest::say("Binding your soul.  You will return here when you die.");
      quest::castspell($userid,2049);
    }
  } 

  #your standard default.pl EVENT_SAY behavior
}
Here, we have built an other level of precedence, where the standard variable $mname allows default behaviour specific to all soulbinders. Thus we do not need the 1234.pl file for each soulbinder anymore (unless your soulbinder also holds a specific quest, in which case you can still explicitly call sqtdefault::EVENT_SAY, but ... this is a bit complicated)
This was for EVENT_SAY, the same holds for EVENT_ITEM, EVENT_ATTACK ... (e.g. you want all guards to spawn more guards in reinforcement after a while when attacked).

We now don't need the 1234.pl file, but the default.pl is more complicated. To do this, we used a if(foo){bar} construct. If we want the same for Guard*, Merchant*, .. it takes as many additional if statements.


Now assume we want to have more 'categories', i.e. not only soulbinders or guards, but also have a specific default behaviour for elves / dwarves / humans / other, and also followers of Karana / Tunare / ..., and/or have NPC belong to guild, have interest in cooking or music, etc etc
First, we must use global variables, as not all this information is available,
Second, the default file becomes a mess of intricate if/else /elsif ; you get many less files, but one has become a monster (level 99).

That's were we want to use plugins. take a look at the following :

There is a dispatch function, that check all required variables, and calls the appropriate subroutine.
Hey wait ! How many subroutines ? If that's writting 1000 subs, i'd rather do quests one by one.
Naa. Using precedence and fallthrough, with 4 categories of 10 values each, you write 40 scripts, get 10000 NPC types.

This is the dispach function, in plugin.pl
*** edited ***
Code:
#this is the main controller routine for default quests
sub dispatch{
  my($pack, $filename, $line, $subr, $has_args, $want_array)=caller(1);

  #$debug && quest::say("[debug]in dispatch");
  #$debug && quest::say("[debug] package : $pack");
  #$debug && quest::say("[debug] subroutine : $subr");

  #get all variables in caller's scope

  # first, we want to cleanup what was set by previous call
  undef $job;
  undef $interest;
  undef $guild;
  undef $mrace;
  #$debug=0;

  no strict 'refs';
  my $package;
  ($package=$subr) =~ s/::\w+// ;
  my $stash = *{$package . '::'}{HASH};
  my $n;
  foreach $n (keys %$stash) {
    my $fullname = $package . '::' . $n;
    if( defined $$fullname){
      $$n=${$fullname};
      #uncomment to get report of what is available
      #quest::say("$n -> $$n (eqiv to $fullname)\n");
    }
  }

  #$debug && quest::say("checking event");

  #this looks for the correct routine to use, based on l=globals and event type
  if(defined $subr){
    my $event;
    if($subr =~ /EVENT_SAY/) { $event="say";}
    if($subr =~ /EVENT_SLAY/) { $event="slay";}
    if($subr =~ /EVENT_DEATH/) { $event="death";}
    if($subr =~ /EVENT_SPAWN/) { $event="spawn";}
    if($subr =~ /EVENT_ITEM/) { $event="item";}
    if($subr =~ /EVENT_ATTACK/) { $event="attack";}
    if($subr =~ /EVENT_WAYPOINT/) { $event="waypoint";}

    #now lookup the routine, and return after first match.
    #the following assumes npc have a $job, $mrace and $guild global
    # This is where precedence takes place :
    #   first look for an interest oriented event, then a job oriented match,
    #   then race dependant, then guild ...
    #   whatever you set as a global category for the mob
    #   If guild behaviour is more important (or more specific)
    #   than race or job, for example, move the line up.
    #   zone usually comes last, as it allows to reproduce the genuine
    #   'default.pl' behavior.
    # returning ensures you don't get 2,3 or 4 answers for an event

    #$debug && showvars();
    defined $interest && defined &{"$interest$event"} && &{"$interest$event"} && return;
    defined $job && defined &{"$job$event"} && &{"$job$event"} && return;
    defined $mrace && defined &{"$mrace$event"} && &{"$mrace$event"} && return;
    defined $guild && defined &{"$guild$event"} && &{"$guild$event"} && return;

    #eventually revert to the standard per-zone default.pl
    defined &{"$zonesn$event"} && &{"$zonesn$event"} && return;

    # we came here if there was no match (i.e. no specific routine
    # for that event)
    # do nothing then ? or ...
    defined &{"default$event"} && &{"default$event"} && &{"default$event"} && return;
  }

  #we very unlucky to get here
}


sub showvars{
  my($pack, $filename, $line, $subr, $has_args, $want_array)=caller(1);
  #get all variables in caller's scope
  no strict 'refs';
  my $package;
  ($package=$subr) =~ s/::\w+// ;
  my $stash = *{$package . '::'}{HASH};
  my $n;
  foreach $n (sort keys %$stash) {
    my $fullname = $package . '::' . $n;
    if( defined $$fullname){
      $$n=${$fullname};
      #uncomment to get report of what is available
      quest::say("$n -> $$n (eqiv to $fullname)");
    }else{
      defined &$fullname && quest::say("function $fullname is defined");
    }

  }
}
It's pretty short, showvars() is only here for debug support

Now, the default.pl becomes
Code:
sub EVENT_SAY{
  $plugin::debug && quest::say("[debug]in qstdefault::EVENT_SAY");
  #plugin::showvars();
  plugin::dispatch();
}

sub EVENT_ITEM{
  plugin::dispatch();
}

sub EVENT_DEATH{
  plugin::dispatch();
}

sub EVENT_ATTACK{
  plugin::dispatch();
}

sub EVENT_SPAWN{
  plugin::dispatch();
}

sub EVENT_TIMER{
  plugin::dispatch();
}

sub EVENT_SLAY{
  plugin::dispatch();
}

sub EVENT_WAYPOINT{
  plugin::dispatch();
}
This was a simple example. You need not respond to all events, and you can also process them directly in the default.pl, without disaptch support.

I'll give a few examples of category scripts further on.

In the system i use, there are 4 categories :
job : it is the occupation of the NPC, whether guard, shopkeeper, jailor, ... This not necessarily the class of the NPC
race : self-explanatory. Huh, except that Freeport citizen, Qeynos Citizen, Freeport Guards, Qeynos guards, Humans all have the Human race.
guild : what guild the npc belongs to, if any
interest : some hobby or special knowledge the NPC has (eg legends / hunting / politics / weapons ...)

When an event is triggerred on default quest, it goes to dispatch.
Dispatch finds out the event and the variables set for the NPC.

It then checks, in order :
1. if a $interest exists (not all NPCs have an interest),
2. if so looks for the category routine on the trigger event, and calls it. It is just a regular routine, except it returns a value : 1 if the event was treated, 0 if not.
3. if the event was treated, we're done, then return
that was first line :
Code:
defined $interest && defined &{"$interest$event"} && &{"$interest$event"} && return;
If not proper treatment was found, job category is checked. Again, we go through steps 1, 2, 3 above
If there was a treatment (got return value 1), we return.
Else, again with the $mrace category. Return 1 upon process.
If still nothing : go for the guild
... as many categories as you want.
last, we check for plugin on the zone, which gives the same behaviour as checking the zone/default.pl
If you didn't get a result, and still want to process the event, you might explicitly call a plugin subroutine to process the event (e.g. a log function)


To do this, you need to define the globals for the npc you want, and the subroutines. I'll go with the soulbinder example :

define the globals in database. You may want to also set zoneid, but 0 is just fine :
Code:
-- now do the soulbinders. Hope they have all a Soulbinder_xxxx name
insert into quest_globals (npcid, name, value, zoneid, expdate)
  select npc_types.id, 'job', 'soulbinder', zone.zoneidnumber, unix_timestamp(now()+1000000)
    from npc_types, spawnentry, spawn2, zone
    where name like 'Soulbinder%'
    and npc_types.id=spawnentry.npcID
    and spawn2.spawngroupID=spawnentry.spawngroupID
    and zone.short_name=zone;

-- also set qglobal flag
update npc_types, spawnentry, spawn2, zone  set qglobal=1
    where name like 'Soulbinder%'
    and npc_types.id=spawnentry.npcID
    and spawn2.spawngroupID=spawnentry.spawngroupID
    and zone.short_name=zone;
and define a subroutine to answer EVENT_SAY. The name must be <job><say>, e.g soulbindersay. This is defined in dispatch function.
The file name it is in does not matter,as long as it is loaded as a plugin.

Here is the routine.
Code:
#the typical soulbinder. Flag a global 'job' to 'soulbinder' on such NPCs

sub soulbindersay {
  if($text=~/Hail/i){quest::say("Greetings $name. When a hero of our world is slain  their soul returns to the place it was last bound and the body is reincarnated. As a member of the Order of Eternity  it is my duty to [bind your soul] to this location if that is your wish."); return 1;}
  if($text=~/bind my soul/i) {
    quest::say("Binding your soul.  You will return here when you die.");
    quest::castspell($userid,2049);
    return 1;
  }
  return 0;
}
Note the return statements. They are very important.
This is because soulbinders can be dwarves, or elves, or in a guild...

This is a basic dwarf plugin :
Code:
sub dwarfsay{
  if($text =~ /Dwarves smell/i){
    quest::say("How dare you insult our noble race !!!");
    quest::shout("Smell my hammer, then !");
    quest::attack($name);
    return 1;
  }
  return 0;
}
and a troll one
Code:
sub ogresay{
  if($text =~ /Dwarves smell/i){
    quest::say("Yap !!! Dey taste too. Hu Hu HU");
    quest::emote(" laughs loudly");
    return 1;
  }
  return 0;
}
In case soulbindersay returned 1, the race behaviour is not called. If it returns 0, it is always called (if it exists). In the latter case, saying "Dwarves smell" to a dwarven soulbinder wouldn't incur anything.


You would typically put these into soulbinder.pl, dwarf.pl and troll.pl in your plugin directory, but it does not matter. Only the sub names matter.

Notes & Hints & Tips
* Don't forget to enable qglobal for the npc_type ! This can not be changed live.

* The plugin loader shows no loading status, and chokes only on big syntax errors. I'm trying to fix this. Until then you can use the templates in this post, or test your scripts with the simulator

* It is very important how you organize your fallthrough. Go from most specific to least specific. Else "Hail. Do you know where the Dwarf smelling Artifact of Kronnuzic was buried ?" will never be answered correctly by the dwarven soulbinder, even if any baby dwarf knows the answer, because Hail was matched by soulbinder code.

* For the same reason, don't hook too many events in top level categories. less is more.

* There is no need to define all the event responses. E.g. the soulbinder job plugin only sets EVENT_SAY response. If no routine is define, the dispather considers a miss, and falls through to search routine next variable (e.g.after job, it would be mrace ; if you had an EVENT_ATTACK, if no soulbinderattack, then dwarfattack would be tested for, then ... until kaladimaattack, then maybe you have a final default script)
The above examples use interest, job, mrace, guild and zonesn. Only zonesn depends on actual zone definition, as this variable is not set in th DB. Thus it will be kaladima if that's the zone you are in. This is easy to work around if you need to.

* You can call any specific routine from any other within plugin package (what you can't do with default.pl, i.e. calling qst1234::EVENT_SAY does not work)
moreover, you can call plugin::whatever from any 1234.pl quest, but you must use dispatch (it converts the variables from package to package).

* You can alter the quest_global zoneid and charid as you want . Setting charid allows to prepare test quests on a running server where it only affects tester's char, while playing around with zoneid allows to tune the var as if you had the same or different 1234.pl for a NPC in different zones.

* You can turn on/off whole or part of your scripts by changing the variables in the DB (qglobal in npc_types.qglobal or directly in quest_globals). This takes effect immediatly. No reload need or whatever. But you can't alter calls to zone related (at least unless you have modified dispatch )

* You can in the same way affect dynamically scripts to NPC from scripts, using quest::targlobal or quest::setglobal
you can write one-shot scripts, using quest::delglobal

* setting 'debug' to non-zero as global variable for a NPC allows to turn tracing / debug on in plugin,
Use '$plugin::debug && quest::say("[debug] in foo with variable bar : $bar");' in sub foo. plugin::debug keeps set until you trigger to a NPC that a 'debug' set to 0.

* Be carefully not to scope your own varaibles with the ones defined by the embparser. Use my



Going a bit further
it is possible to install filters, using a top-level processing that would count/match/translate the events. In this case, process the event, and return 0. It will go through lower levels as if it had not been processed.
a simple example is formatting, as you can filter $text through s/\s+/ /g, and pass along to lower levels. This helps a lot to write simple matching patterns.

filters can also set variables, do aliasing, ...

That's all for the moment. I'll give more script examples in a later post.

i hope it can be useful.
Reply With Quote
  #2  
Old 02-29-2004, 04:48 PM
Lurker_005
Demi-God
 
Join Date: Jan 2002
Location: Tourist town USA
Posts: 1,671
Default

Very nice work!

Stickied
__________________
Please read the forum rules and look at reacent messages before posting.
Reply With Quote
  #3  
Old 03-03-2004, 11:19 AM
smogo
Discordant
 
Join Date: Jan 2004
Location: 47
Posts: 339
Default

k, as of 0.5.3dr3, there are a few issues for using this, so maybe not everyone could try it.

i posted about some of the issues, esp. global var retrieval, perl variable set/reset, and multiple plugins cache problems. There are a few fixes to apply to the server if you want to have this running.

So i decided to set up a server so that people could play around and figure out by themselves. The server name will be Khalzed Dur perl quest test Server or something like that. i'll go into details there.

i wrote a few scripts, but they are'nt too crafted, as my english is very poor, and spent a lot of time in debugging plugins / variables / globals

jobs :
brother.pl applies to religious monk brothers, or hermits.
guard.pl applies to guards. Usually set 'job' to 'guard' for all ... guards
soulbinder.pl gets the NPC to be a soulbinder
innkeeper.pl applies to innkeepers

zone :
started to write a script freporte.pl for Freeport East area. This script applies to all NPC that are in freporte, when no other match applies.

interests :
religion.pl is knowledge about religious facts (this one is based on a file in EQ directory)


They were built automatically, so they may react odd sometimes. i also used another plugin i'll discuss later, the gaagle function.
Reply With Quote
  #4  
Old 05-20-2004, 08:44 AM
KhaN's Avatar
KhaN
Dragon
 
Join Date: Mar 2004
Location: France, Bordeaux.
Posts: 677
Default

Does this still work with 5.7DR3 ?
Actually, following this, i cant get it to work.
__________________

Reply With Quote
  #5  
Old 05-24-2004, 12:50 PM
smogo
Discordant
 
Join Date: Jan 2004
Location: 47
Posts: 339
Default

i did not move to 5.7dr3; but there should be no problem, as the whole thing is 100% perl.

There are a few fixes not yet in the code AFAIK, regarding :
- having more than one plugin file. i posted a fix for this, but it's not in the std distro. You can work it around by pasting all your plugins to one file (e.g. eqemu\plugin.pl)
- using precedence in global variables. The actual code AFAIk only allows one variable of same name for one pc or npc or zone. I posted a fix for this (think the name was something like "wildcard behaviour"). You can do without it, but it's not so good. Read again the sticky, you might find your way ...
- note that variables aren't cleared between talks to a NPC, so a global set for a NPC (e.g. job=guard) remains set on the next NPC, so, example again, once you talk to a 'guard', all NPCs are 'gaurds' until they are explicitly something else. There was a post named like 'perl variable mess' or something .... For this reason it is good to use wildcards, and set the global to "__none__" or something like that for all (i.e. npcid=0, zone=0, charid=0)

Debug hints :
- ensure plugins are loaded : once the plugin subroutines for a class are defined, call them directly in a quest file to make sure they are all loaded properly. You can also use the showvar and showfuncs subroutines (search, you'll find )
- check what global variables are declared and appropriate for your NPC (use showvars, or Lurker's debug default debug script). Ensure the global variable (e.g. 'job') is defined for your NPC
- the 'dispatch' plugin should be called from the default.pl file. Track the calls, until you reach the bottom of dispatch when no global has been defined.

i hope this helps .
__________________
EQEMu Quest Repository is down until something new :(
Reply With Quote
  #6  
Old 07-31-2004, 03:37 PM
bbum
Banned
 
Join Date: Apr 2004
Posts: 245
Default

Quote:
sub dwarfsay{
if($text =~ /Dwarves smell/i){
quest::say("How dare you insult our noble race !!!");
quest::shout("Smell my hammer, then !");
quest::attack($name);
return 1;
}
return 0;
}
what is return 1 and return 0 i dont understand, will this not work?


Quote:
sub EVENT_SAY{
if ($text=~ /Hail/i){quest::say("Do you accept the mission or do you wish to leave?");}
if ($text=~ /leave/i){quest::say("How unfortunate.");
quest::attack($name);
}

}
Reply With Quote
  #7  
Old 07-31-2004, 04:02 PM
KhaN's Avatar
KhaN
Dragon
 
Join Date: Mar 2004
Location: France, Bordeaux.
Posts: 677
Default

No this wont work, because you wont recall the plugin.
__________________

Reply With Quote
  #8  
Old 07-31-2004, 04:11 PM
bbum
Banned
 
Join Date: Apr 2004
Posts: 245
Default

can you explain? what do i need to change? whats the plugin?
Reply With Quote
  #9  
Old 08-07-2004, 07:56 PM
bbum
Banned
 
Join Date: Apr 2004
Posts: 245
Default

khan- can you explain plz =(

itll work once and then not again or something?
Reply With Quote
  #10  
Old 08-07-2004, 08:31 PM
KhaN's Avatar
KhaN
Dragon
 
Join Date: Mar 2004
Location: France, Bordeaux.
Posts: 677
Default

Smogo
Quote:
In case soulbindersay returned 1, the race behaviour is not called. If it returns 0, it is always called (if it exists). In the latter case, saying "Dwarves smell" to a dwarven soulbinder wouldn't incur anything.
it look pretty clear, just reread what smogo wrote, i dont feel the use of explain 2 times the same thing when first time, its pretty clear ...
__________________

Reply With Quote
  #11  
Old 08-07-2004, 09:21 PM
bbum
Banned
 
Join Date: Apr 2004
Posts: 245
Default

if im not using race behavier do i still need to use the return commands?

i read through that before but it confused me
Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump

   

All times are GMT -4. The time now is 12:55 PM.


 

Everquest is a registered trademark of Daybreak Game Company LLC.
EQEmulator is not associated or affiliated in any way with Daybreak Game Company LLC.
Except where otherwise noted, this site is licensed under a Creative Commons License.
       
Powered by vBulletin®, Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Template by Bluepearl Design and vBulletin Templates - Ver3.3