Loading...   

[Show Table of Contents]


A guide to start new devs on creating scripts with Perl for their emulated Everquest environment!

 

§Why Choose Perl?

Perl was, to the best of the author's knowledge, the original scripting language chosen for the Everquest Emulator community as the go-to scripting language. With time, there has been a fairly major push for/to LUA, but many of the new features for LUA are exported in Perl as well. Perl can be found on nearly all Linux distributions, by default, and can even be leveraged against high end networking equipment such as routers, switches, and firewalls. Perl has nearly unlimited resources available on the web from all kinds of sites. In the opinion of this article's author, the following sites are good resources for examples: PerlMaven.com, McGill School of Computer Science's Tutorial, PerlMonks.org, or StackOverflow.com all are excellent.

 

§How/Where to Create Perl Scripts?

Perl scripts are located in one of two places. The quests folder and the zone (or global) folder. In example: 

C:\path\to\EQEmuServer\quests\freporte\MyNPC.pl

For Perl plugins, this may look something like this:

C:\path\to\EQEmuServer\plugins\MyPlugin.pl

 

Perl scripts are generally created for a specific NPC(s), a zone's default.pl, or the global scripts global_player.pl or global_npc.pl. Each one serves a specific purpose. The call heirarchy for Perl scripts looks like this:

NPCs: If global_npc.pl exists, run it. If named/ID script in /quests/global/ for NPC exists, run it. If zone-local named/ID script for NPC exists, IGNORE GLOBAL VERSION and run zone-local version. If no zone-local script exists, use zone's default.pl script.

Players: If global_player.pl exists, run it. If zone-local player.pl exists, run it next.

(Assume '...' is replacing C:\path\to\EQEmuServer\ )

...\quests\global\global_npc.pl   # RUNS
...\quests\freporte\orc_pawn.pl   # RUNS
...\quests\freporte\default.pl   # RUNS...but cannot affect orc pawn due to script above.

...\quests\global\global_npc.pl   # RUNS
...\quests\global\orc_pawn.pl   # IGNORED, zone-local version exists in zone 'freporte'
...\quests\freporte\orc_pawn.pl   # RUNS

...\quests\global\global_npc.pl   # RUNS
...\quests\freporte\default.pl   # RUNS, affecting ALL npcs without specific script in zone

 

§Specific NPC(s):

This can be accomplished by creating a script in the zone of choice, using the filename of the NPC's npc_types name or the NPC's npc_types ID. This name MUST exactly to the NPC's database name. If the name is "#My_Silly_Monster" (no quotes), then the quest script should be "#My_Silly_Monster.pl". Additionally, symbols like single quotes or the backtick are simply replaced with a hypen (-) in the file name. To give some examples:

## These two are for the same NPC
C:\path\to\EQEmuServer\quests\freporte\Sir_Artanis.pl
C:\path\to\EQEmuServer\quests\freporte\8074.pl

## The (-) is replacing the (`)
C:\path\to\EQEmuServer\quests\neriaka\Andara_C-Luzz.pl

Either one of these would work as Sir Artanis has the npc_types ID of 8074 and his name field is exactly 'Sir_Artanis'; I could use either file name, BUT NOT BOTH, to create actions for Sir Artanis of East Freeport. 

CAVEAT: Using specific NPC files applies to ALL NPCs with that name or ID. This means that if one were to have 'orc_pawn.pl', ALL ORC PAWNS IN THE ZONE WOULD RESPOND TO THIS FILE. 

 

§Zone-local 'default.pl':

This is an easy way to handle multiple actions, across multiple NPCs, in a single file. It has the benefit of keeping everything together, which allows for repetitious code to be reused easier through the use of custom subroutines, well-constructed conditional statements, or (lazily) copy/paste. (It is the author's opinion that this is the easiest way to handle dynamic zones. Specific NPCs, such as complex bosses or BBEG (Big Bad Evil Guy, or endboss) are better suited outside of this due to ease of adjusting/expanding the zone down the road.) An example filename can be seen below:

C:\path\to\EQEmuServer\quests\freporte\default.pl

That's it. Really! No craziness, no database tying with NPC name(s) or ID(s).

CAVEAT: Script local variable variables CANNOT be used and will render the file inert. Make use of EntityVariables for storing ANYTHING that will last past the current code block!

 

§Global Player and NPC Files:

The global_player.pl and global_npc.pl files are fantastic ways to carry common commands, tools, functions, etc across ALL zones. To give a real world example, many of EZ Server's many /say commands exist in the global_player.pl file. This allows a player to utilize these commands in EVERY zone without needing to copy/paste the same code into every single zone's player.pl file. For the global_npc.pl and a real world example, many of EZ Server's pet functions and pet menu actions are handled entirely through the global_npc.pl file. These two files are best suited for reptitious actions of handling certain loot across multiple zones from a singular, easy to manage location (see examples section below).

 

§Global, Specific NPC Files:

The global files for specific NPC(s) are best suited for NPCs that exist in many zones, but hold EXACTLY the same function. A great, real world example is EZ Server's Buff_Bot.pl script. It handles the act of buffing players, regardless of the zone. If a spell effect needs to be added or removed, this file alone can be editted instead of manually fixing multiple files in multiple locations. Utility NPCs, bankers, common merchants are great uses for a global, specific NPC file. An example file path is provided below:

C:\path\to\EQEmuServer\quests\global\Buff_Bot.pl

 

§Intro to Perl

Now that the file hierarchy has been explained, let us dive into actually using Perl. We are going to hit on some basics, some intermediate, and then some really ridiculous items to illustrate the power behind a clever coder and the use of Perl.

 

§Variables:

A variable is the code equivalent of a container. For Perl, it's pretty simple as it is not a strongly-typed language and are simply declared using a dollar sign before the variable name ($). This means it does not require each variable to be defined by it's type (or size). Perl's only concern is 'scoping'. A temporary variable is one that is intended to be used and then tossed. A script 'global' variable is one that is intended to be used throughout the script. In Perl, a temporary or 'local' variable is defined as such by simply prefixing the variable name with "my". See the example below:

$lastVariable = 1; # This variable will persist until the end of the script.
my $shortVariable = 2;  # This variable will expire at the end of the current code block.

To show this off in a real example, see below. Notice how $lastSpoken is created on the fly and is recalled each time the EVENT_SAY block is hit. The $random variable on the other hand expires as soon as the ending bracket from the else block is hit. That is scoping in Perl.  

# this is a made up example using Boomba_the_Big
sub EVENT_SAY
{
	if($text=~/hail/i) # if the text from player contains "hail", case insensitive
	{
		if($lastSpoken && $lastSpoken ne $name) # insult previous player...so long as previous player isn't also current player
		{
			quest::say("You not as mean as $lastSpoken! Me like!");
		}
		else # there is no previous player
		{
			my $random = int(rand 4); # pick a number, 0 - 3
			if($random == 1) # pick on player 
			{
				# use of the variable $name is a pre-exported variable for EQEMU
				quest::say("Wow $name, you not pretty like Boomba...");
			}
		}
		$lastSpoken = $name; # store name of the client that just spoke with Boomba
	}
}

 

§Conditionals:

Conditional statements are how developers and admins create 'flow' or logic. This allows the creation of chains of events based on the setup of the conditional(s). With Perl, the standard conditional elements occur (if, elsif, else) and the standard operators (<, <=, >, >=, ==, !=, eq, ne=~). For numeric (number-based) checks, the standard mathematical operators and "==" or "!=" may be used. For text or string conditionals, utilize the "eq", "ne", or "=~" (the last one with regex, or regular expressions). Additionally, to expand a conditional, use the double pipe "||" (OR) or the double ampersand "&&" (AND) to check against more than one thing. See below for a fairly detailed breakdown of each:

# Again, picking on Boomba_the_Big.pl
# $ulevel is a pre-exported variable for user, or client, level

sub EVENT_SAY
{
	if($text=~/hail/i)
	{
		if($ulevel < 60) # if player less than level 60
		{
			quest::say("Hah, hello puny!");
		}
		elsif($ulevel < 65) # if player is between 60-64
		{
			quest::say("Heh, hello wee one.");
		}
		elsif($ulevel <= 70) # if player is between 65-70
		{
			quest::say("Uh, hello $name"); # ooh, calling us by name! how fancy!
		}
		else	# at this point, we're above 70, let's make him panic
		{
			quest::say("G-g-good morning, how be?"); # yeah, better stammer big boy!
			quest::emote("gulps audibly."); # emote effect
		}

		if($name eq 'Goomba') # if the user name is equal to 'Goomba'
		{
			quest::say("Hey! You in wrong game!");
		}
		elsif($name eq 'Roomba' || $name eq 'Zoomba') # if name equal to 'Roomba' or if name equal to 'Zoomba'
		{
			quest::say("Hey! No robot!"); 
		}
		elsif($name =~/oomba/i) # if name contains 'oomba' anywhere in it
		{
			quest::say("Are we 'lated?");
		}
		elsif($name eq 'Hateborne' && $class eq 'Wizard') # if the player name name is exactly 'Hateborne' and this player is a wizard
		{
			quest::say("Please, no hurts again!");
		}
		
		if($class ne "Necromancer") # if the class variable is NOT Necromancer
		{
			quest::say("Me glad you not stinky neck-rah-mancah."); 
		}
	}
}

 

§Arrays:

Arrays are a quick, easy way to store a lot of IDs, names, or whatever like-typed data one can come up with in development. Arrays are declared using the 'at' symbol (@) rather the the dollar sign ($). They are also assigned data slightly differently as arrays hold a list of things, whereas a standard variable holds ONE item. Arrays are accessed in a slightly different way as well, since one cannot just tell it to pull data from a list without specifying WHERE in the list to pull it. See below for an example:

# Boomba_the_Big.pl should be named Boomba_the_Victim.pl when we get through
sub EVENT_SAY
{
	if($text=~/hail/i)
	{
		my @name_calling = ('wimp', 'weakling', 'wee one', 'horse hand', 'rodent fearer'); # storing four insults as strings in an array named @name_calling
		my $random = int(rand 4); # randomly pick 0 - 3
		my $insult = $name_calling[$random]; # this will pull in the insult into variable form, notice how we used the $ instead of the @ to specify the array element
		quest::say("Heh heh, hello $insult...how be it?"); # this will dump out the insult into chat, making it look natural
	}
}

Array elements are counted FROM ZERO to the max number. If one were to issue "my $elements = scalar @name_calling;" in the example above, it would give one four. However, if one tried to call element four using "$name_calling[4]", it would fail as the last element is actually 3 (0, 1, 2, 3 is four slots). This is because the scalar @array gives one the number of elements, not the last number used. Generally, the number of elements minus one will return the last element!

Always, ALWAYS remember that Arrays (and most machine items) starting counting from ZERO, not one.

 

§Hashes:

Hashes are the seemingly complex, hugely powerful way to store dynamic data in an easy to access fashion. The work exceedingly well for handling class specific gear from quest turn ins. A hash is declared using the "%" symbol. Hashes are made up of "keys" and "values". A key can have a singular value or multiple values, accessed in an array setup. Accessing hash keys and values are slightly different than normal variables or arrays. They are utilized via "$hashName{keyValue}" for single value per key or "$hashName{keyValue}[elementSlot]" or multiple values per key. See the example below for more details:

# Boomba_the_Big.pl is being mouthy as always...
sub EVENT_SAY
{

	my %classInsults = (
		"Warrior" => [ 'pansy in plate', 'fancypants', 'earl of recycling'],
		"Necromancer" => [ 'deadpoker', 'zombie groper', 'grave robber'],
		"Ranger" => [ 'willing sacrifice', 'ressurection practice', 'bugger']
	);

	if($text=~/hail/i)
	{
		my $insult; # initialize variable for our insult
		if(exists($classInsults{$class}[0])) # if a key for the class hailing him exists, pick an insult
		{
			$insult = $classInsults{$class}[int(rand 3)]; # pick a random insult string, notice that we are again using the $ sign instead of the % sign to access a specific key/value pairing
		}
		else  # the class isn't shown above
		{
			$insult = "sewer rat";
		}
		quest::say("Heh heh, hello $insult...how be it?"); # this will dump out the insult into chat, making it look natural
	}
}

 

§Building Subroutines:

Building a custom subroutine is a great way to reuse code in a portable, clean fashion without pasting the same block a dozen times over in the quest script. Each subroutine must contain a return statement (even a blank return) as EQEmu seems to hang on occasion without that return statement. Once a subroutine is name, created, and filled out then all one needs to do is call it by its name, passing any variables as needed. When passing parameters, an array is automatically created with the variable name "@_". Each passed argument will be in order passed in this array. Accessing these items is as simple as "$_[0]". See the example below:

# Boomba_the_Big.pl is becoming Boomba_the_Boring.pl
# $ulevel is a pre-exported variable for user, or client, level

sub EVENT_SAY
{
	if($text=~/hail/i)
	{
		my $insult = Insult($class); # get the insult string
		quest::say("Hey! You! ...Yeah, $insult, you!"); # use it to insult the player
	}
}

sub Insult
{
	my $class = $_[0];  # pull down the first passed variable
	if($class eq "Warrior") { return "pansy in plate"; }
	elsif($class eq "Cleric") { return "priestly pansy"; }
	elsif($class eq "Ranger") { return "rezz fodder"; }
	else { return "useless worm"; }
}

 

§Accessing Objects and Pre-Exported Variables:

On the Ultimate Perl Reference sheet, many pre-exported variables are created and ready for use. Some are event specific, such as $text being exclusive to EVENT_SAY or $item1 (or 2/3/4) to EVENT_ITEM. Those are mostly self explanatory or well documented. The objects are slightly, well, less clear. For EQEmu, all NPCs and characters (referenced as client from here on) are of the base type "Mob". This means that both NPCs -AND- Clients have access to the Mob objects. There are some that will have little/no effect to either side in the Mob class, but both types of entities branch out from the Mob base. See the example below of a Mob object being utilized on an NPC and a Client.

$npc->GetCHA(); # used in an NPC script, would return its Charisma stat
$client->GetCHA(); # used in an client script, would return his/her Charisma stat

To expand on this, the NPC class and it's objects CANNOT be used by clients (and vice versa) as the calls for each are unique to that branched off class. To elaborate, why would a player ever have an NPC grid or why would an NPC have a loginserver ID? Understand the difference between NPC and Client now? Good, moving on! Some of the calls are not explicitly well defined on the Perl Reference sheet. This is being worked on as developers have time, but this is still highly accurate and fairly well maintained. 

 

 

§Examples

§NOTE: All file names do NO include "ez_" in the actual file name. This was done simply to avoid duplicate Wiki pages.

EZ_Tsukasa.pl - A global, NPC-specific script (Tsukasa.pl) from EZ Server

EZ_HOH_default.pl - A zone-local default.pl script to handle all the activities in the Halls of Honor (a) zone from EZ Server (Drop chances replaced with XXX)

EZ_global_npc.pl - An excerpt from EZ Server's global_npc.pl file to show how to add loot in various zones from one location. (Drop chances replaced with XXX)