RunUO Community

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Scripting for Dummies: First Script, Creating an Item

Status
Not open for further replies.

David

Moderate
Scripting for Dummies: First Script, Creating an Item

Scripting a RunUO Item for Dummies.

Lesson 1: A basic Item.
Lesson 2: The RunUO Docs.
Lesson 3: Range checking in both distance and time.

NOTE: This topic was first written for RunUO version 1.0 but fully applies to 2.0. - David


This will take you through the creation of a RunUO script written in C# (pronounced C sharp) which will add a unique item to the game. We will then expand on the Item by giving it some functionality. Although I will endeavor to introduce some key C# concepts here, it would help immensely if you read up on C# a bit first. The script I will walk you through, a Magic Eight Ball, was not written by me and unfortunately I do not know who the original author is. If this is your script please contact me and I will give you credit.

C# source files or scripts are simply text files. You may use any text editor to create or edit them including Windows Notepad. However there are several free programs that are designed to make editing C# easier; for example SharpDevelop, or Visual C# Express (see important note below) from Microsoft. Any of these products will make scripting easier and faster and prevent many of the most common mistakes. (My recommendation would be Visual C# Express.)

C# uses "Object Oriented Programming." Anything created in C# is an Object and any given Object is referred to as an Instance of that Object. We describe or define objects by writing Classes. There is actually a fundamental Class that defines "Object" itself. A Class that defines an object will be Based on another Class. When one Class is based on another it can Inherit the features of the base, or Parent, Class. It can also serve to further refine or add to the parent Class. For example in RunUO, there is a Class (or object) called Dagger which is based on BaseKnife which is based on BaseMeleeWeapon which is based on BaseWeapon which is based on Item which is based on Object. Each Class that is based on, or Derived from an earlier Class either adds functionality or refines the definition of the object, or both. There are certain things that all Weapons share so those things are defined in BaseWeapon. Likewise, There are certain things all knives have in common so those are defined in BaseKnife. When we finally get to defining the actual Dagger, there is little left to do but specify the proper graphic to use and how much damage it can do.

RunUO has two base Classes which we as scripters can build on. Those are Item and Mobile. Both Item and Mobile are defined in the RunUO core software and are not available for us to edit or view directly. But we can easily build on them to create new objects. We can define a new Class which is based on either Item or Mobile, or any of the many child Classes already defined in the RunUO distribution scripts. We can add or change Properties which define the "style" of our object or add or change Methods which define the "functionality" of our object. For detailed information on what objects are defined in RunUO, the Hierarchy they are defined in, and what Properties and Methods they expose look at Overview.html in your RunUO\Docs directory (more on this in lesson 2.)

I will introduce one more C# concept as we get started on our script. Namespaces. The concept of namespaces is how C# organizes Classes and other program components. A namespace is a set of related Classes. A namespace may also contain other namespaces. When you make a reference to another Class you must include in your reference the namespace that Class exists within, unless the two share the same namespace. You may complete this reference one of two ways; by explicitly stating the namespace as part of the name of the Class, or by use of the using directive at the beginning of your script. For C# and .NET arguably the most important namespace is called System, for RunUO it is Server. Therefore most RunUO scripts start out with
Code:
using System;
using Server;
Including those directives (a directive is an instruction to the compiler) allows us free use of any Classes in those namespaces. Next we must identify the namespace our Class exists within, in this case it will be "Server.Items".
Code:
using System; 
using Server; 

namespace Server.Items 
{
Note that each statement in C# ends with a semicolon, however the namespace keyword indicates that we are identifying a block of code rather than issuing a command. Blocks of code in C# are contained within a matched set of curly braces {}. For every opening brace there must be exactly one closing brace. Mismatched braces are the most common scripting error.

Now we are ready to define our Class. We will be creating a Class called "EightBall" which will be derived from (or based on) the Item Class. Our Class will be accessible from any part of RunUO provided it is referenced properly, therefore it is said to be "public." After the public modifier we declare that this is indeed a Class and it's name followed by a colon and the name of the parent Class which this Class is to be based on. Since our parent or base Class is Item, we inherit all the features of the Item Class. Again note that the class keyword defines a block of code.
Code:
using System; 
using Server; 

namespace Server.Items 
{ 
	public class EightBall : Item
	{
You begin here to see the indentation most programmers use when writing code. This helps to keep straight what is happening and at what level. Now we will begin to add Methods to our Class. These methods are C# code that specifies how things are supposed to happen, or the method by which these things occur. The order that we put the methods in does not matter, the compiler takes care of making sure the right one is called at the right time. Normally though you will find methods dealing with creation of the object at the top and methods dealing with saving the object during a World Save at the bottom.

The first method we will add is called the constructor. The constructor is called when an "instance" of the object is created. Every object must have at least one constructor, but often there are two or more. If the object may be placed ingame by a GM it must be marked by the tag "Constructable." Tags immediately precede a method and are contained within square brackets []. The basic form of our constructor is this.
Code:
		[Constructable]
		public EightBall() : base( 0xE2F ) 
		{
		}
Note the name of the constructor method is the same as the name of the Class and that it is declared as public and marked Constructable. The EightBall Class was previously identified as based on the Item Class, and one of the constructors of the Item Class accepts a single integer value which becomes the ItemID or the number of the graphic used to display the Item. (Graphic numbers may be found using a program called InsideUO.) Therefore when we first create an instance of an EightBall, we tell the base Class we want to use an ItemID of 0xE2F which is hexadecimal for 3631. (You may use either hex or decimal, but most programmers eventually use hex.)

Within the constructor method we should set any properties of the object that need to be set. There are two properties of the Item Class we will set here for all instances of our object. Those are Weight and Name. Our final constructor looks like this.
Code:
		[Constructable]
		public EightBall() : base( 0xE2F ) 
		{
			Weight = 1.0; 
			Name = "a magic eight ball"; 
		}
In RunUO all Items also need a "serialization" constructor. This constructor is called for any specific instances of our object during a World Load (when you start up the server.) Normally the serialization constructor contains no code and is not tagged. The constructor must accept a Serial number from the server and pass it on to the serialization constructor of the base Item Class.
Code:
		public EightBall( Serial serial ) : base( serial ) 
		{ 
		}
During every World Save the Serialize method of every object is called. It is the job of the Serialize method to record the state of the object. Any information specific to this instance of this Class should be saved at this time. Keep in mind the base Item class will likewise record any information it is responsible for. In this case all we have done is set the Weight and Name both of which the Item class is responsible for. All we will do is accept the reference to the GenericWriter object and pass it on to the Serialize method of our base Class. We also use the GenericWriter to save one integer, a zero, to denote the version of this object. If we modify the object in the future and must add to the information we save, this number will increment.
Code:
		public override void Serialize( GenericWriter writer ) 
		{ 
			base.Serialize( writer ); 
			writer.Write( (int) 0 ); 
		}
Now we will add the Deserialize method. This method is called during a World Load to read back in all the information recorded about this instance of this Class in the Serialize method. Serialize and Deserialize work hand in hand. Any information written by Serialize must be read by Deserialize in the exact same order. This is another source of many scripting errors.
Code:
		public override void Deserialize(GenericReader reader) 
		{ 
			base.Deserialize( reader ); 
			int version = reader.ReadInt(); 
		}
Here is the entire script at this point. This is complete in the sense that it will compile and may be created ingame. At its most basic form this is a RunUO Item.
Code:
using System; 
using Server; 

namespace Server.Items 
{ 
	public class EightBall : Item
	{ 
		[Constructable]
		public EightBall() : base( 0xE2F ) 
		{
			Weight = 1.0; 
			Name = "a magic eight ball"; 
		} 

		public EightBall( Serial serial ) : base( serial ) 
		{ 
		} 

		public override void Serialize( GenericWriter writer ) 
		{ 
			base.Serialize( writer ); 
			writer.Write( (int) 0 ); 
		} 
       
		public override void Deserialize(GenericReader reader) 
		{ 
			base.Deserialize( reader ); 
			int version = reader.ReadInt(); 
		}
	} 
}

Next we will add some functionality to our EightBall Class. We will cause the EightBall to send a message to the Player when it is double clicked. Looking in the RunUO Docs, we can find a list of all the methods and properties of the Item Class. One of those is shown as
RunUO Docs said:
virtual void OnDoubleClick( Mobile from )
Virtual means that this method may be overridden by a child Class to change it's functionality. Void means that this method does not have a return value, whatever calls it won't get anything back. OnDoubleClick is the name of the method, and the method expects to receive a reference to an object of type Mobile which this method will call "from." Since the method will be called from elsewhere, we will also declare it public. When you override a virtual method you may not add to or remove any of the parameters of the method, and you must change "virtual" to "override."
Code:
		public override void OnDoubleClick( Mobile from ) 
		{
So at this point the object knows it was double clicked and by whom. Next we will call one of the Random methods of the Utility Class to generate a random number between 0 and 7 (eight choices.) Since Utility is in the Server namespace included at the beginning of the script we are ready to go. We will use the random number as the argument of a switch statement, which will send a different message to the Player for each possible random number.
Code:
		public override void OnDoubleClick( Mobile from ) 
		{ 
			switch ( Utility.Random( 8 ) )
			{
				default:
				case  0: from.SendMessage( "IT IS CERTAIN" ); break;
				case  1: from.SendMessage( "WITHOUT A DOUBT" ); break;
				case  2: from.SendMessage( "MY REPLY IS NO" ); break;
				case  3: from.SendMessage( "ASK AGAIN LATER" ); break;
				case  4: from.SendMessage( "VERY DOUBTFUL" ); break;
				case  5: from.SendMessage( "CONCENTRATE AND ASK AGAIN" ); break;
				case  6: from.SendMessage( "DON'T COUNT ON IT" ); break;
				case  7: from.SendMessage( "YES" ); break;
			}
		}
I took a common shortcut in each case statement. Technically, each case defines a block of code and as such should be enclosed in braces. Instead I placed the entire block on one line, still using a semicolon after each statement. I is important here to know that this
Code:
		case  0: from.SendMessage( "IT IS CERTAIN" ); break;
is the same as this
Code:
		case  0: 
		{
			from.SendMessage( "IT IS CERTAIN" ); 
			break;
		}
Likewise it is important to know that a case statement may not "fall through" to the next statement. Each case must end with a break, a return, or a goto statement.

So finally, our completed script looks like this
Code:
using System; 
using Server; 

namespace Server.Items 
{ 
	public class EightBall : Item
	{ 
		[Constructable]
		public EightBall() : base( 0xE2F ) 
		{
			Weight = 1.0; 
			Name = "a magic eight ball"; 
		} 

		public EightBall( Serial serial ) : base( serial ) 
		{ 
		} 

		public override void Serialize( GenericWriter writer ) 
		{ 
			base.Serialize( writer ); 
			writer.Write( (int) 0 ); 
		} 
       
		public override void Deserialize(GenericReader reader) 
		{ 
			base.Deserialize( reader ); 
			int version = reader.ReadInt(); 
		}

		public override void OnDoubleClick( Mobile from ) 
		{ 
			switch ( Utility.Random( 8 ) )
			{
				default:
				case  0: from.SendMessage( "IT IS CERTAIN" ); break;
				case  1: from.SendMessage( "WITHOUT A DOUBT" ); break;
				case  2: from.SendMessage( "MY REPLY IS NO" ); break;
				case  3: from.SendMessage( "ASK AGAIN LATER" ); break;
				case  4: from.SendMessage( "VERY DOUBTFUL" ); break;
				case  5: from.SendMessage( "CONCENTRATE AND ASK AGAIN" ); break;
				case  6: from.SendMessage( "DON'T COUNT ON IT" ); break;
				case  7: from.SendMessage( "YES" ); break;
			}
		}
	} 
}
I hope you find this useful, next we will add some GM properties and range checking to this script.

Enjoy!

Note on using Microsoft's Visual C# Express--If you download this program, I suggest you click the Previous Versions link and download the 2005 version. If you do download the 2008 version when you set up your RunUO project, be sure to specify that it is a Framework 2.0 project. This is essential if you decide to work with the svn or core files (both somewhat advanced topics.)
 

David

Moderate
If you have not taken the time to look at the Docs yet, you should do so now. In a default installation of RunUO 1.0 Release Candidate 0 the opening index can be found here C:\Program Files\RunUO Software Team\RunUO 1.0 RC0\docs\overview.html. What you are presented with is essentially an API or Application Programming Interface; this is a wealth of information for scripters. The Docs directory also contains lists of all commands, ingame objects, and UO keywords; but those documents are not the topic of this discussion.

Upon opening Overview.html, you will notice the api is first broken down by the Namespaces discussed in the previous lesson. Most often you will find what you need in the Server namespace. Clicking once on Server presents you with a list of Classes in the Server namespace. (Remember that a namespace is a collection of Classes.) Most often you will look for either the Mobile Class or the Item Class. If you click on either one, you will see first the Base Class and possibly any "interfaces" this class implements. You can recognize an interface by its name as it will always start with a capital "I". We will discuss interfaces in detail at a later time. Next you will see a list of Classes derived from this Class followed by any Nested Types or Classes embedded within this Class. Nested types are generally specific purpose things like a timer for a particular function or a special data type. Clicking on any of these items or types will take you to the details for that specific type.

Finally we get to the components exposed by the Class. First are the static properties and methods; anything marked (static) applies to all instances and child instances of the class equally and at the same time. Changing a static property in one instance will change it in every instance of the Class. Next are the constructors which are marked (ctor). An object will always have a default constructor which has no parameters, and anything derived from Item or Mobile will have a constructor that requires a Serial as a parameter. Sometimes there will be additional constructors each requiring different parameters, for example the Item Class has a third constructor that requires a parameter of type int which is named itemID; this is how we pass the graphic index number to the Item Class when we derive a new Class. As an example in the script above we declare the Class as derived from Item with this line public class EightBall : Item and then tell the base Item class which itemID to use with this line public EightBall() : base( 0xE2F ). The compiler knows to use the Item( int itemID ) constructor because that is the only Item constructor which accepts a single integer as a parameter.

Next are the Properties of the Class. Properties will have a return type such as int or bool followed by the name of the property, followed by its accessors. The properties return type specifies what value you can expect when you reference the property in a script. Most often you will see a return type of bool (true/false) or int (whole numbers) although the return type can be nearly anything including another Class; for example several properties return a Mobile. The accessors provided determine if the property is read only ( get; ) or read/write ( get; set; ) We will discuss accessors in more detail the next lesson.

After the properties we find the Methods exposed (made public) by the Class. You will notice in some cases that there are multiple methods with the same name. This is referred to as overloading the method, and each overload will require a different set of parameters. Each overloaded version of the method should perform essentially the same function in the same manner.

In most cases the first item in the method declaration will be the word "virtual." The virtual keyword indicates that this method may be changed in a Class derived from this Class. This is known as overriding the method. When you override a method, the declaration of the method must be identical to the original version including all parameters; with the exception that the word virtual is changed to override. An overridden method may be again overridden in a further derived Class and all overloads of the method do not have to be overridden at the same time. If the word virtual is not present in the declaration of the method, then the method was not intended to be overridden.

The next item in the declaration of the method will be the return type. If there is nothing returned by the method, void will be specified. A return type of some sort is required. If a return type other than void is specified, the body of the method must conclude with a return statement of the proper value type. For example consider the following code:
Code:
 		public bool Dye( Mobile from, DyeTub sender )
		{
			if ( Deleted )
				return false;

			Hue = sender.DyedHue;
		}
This method will generate a compiler error because "not all code paths return a value." The method is declared with a return type of bool, but only if the item in question is deleted will a boolean value actually be returned. To fix this method the scripter should add return true; just before the closing brace.

Finally we see the actual name of the method followed by a set of parentheses. Inside the parentheses will be a list of any (possibly none) required parameters. For example in the OnDoubleClick method used previously the only parameter is listed as ( Mobile from ). This is telling us that the method expects to receive a reference to a object of the Mobile Class which it will refer to as from. When we override this method we may not change the list of parameters, but we can call them whatever we want, for example from often becomes m. That is ok, as long at from or m are still of the type called Mobile.

I hope this discussion will help you to use the Docs as the fantastic source of scripting information that it really is.

enjoy!
 

David

Moderate
The Magic Eight Ball script as it stands now, does its job well but could be improved with a few easy enhancements. Currently there is no range checking, a Player can activate the Eight Ball from across the room, or even from the next room. Also, it has been a lot of years since I have used my Magic Eight Ball, but it seems like you had to concentrate for a few seconds before it worked. I think at the very least we should add a three second delay before our Eight Ball can be used again.

What we need to look at first is the OnDoubleClick method at the end of the script. Currently it consists of a switch statement that makes a bunch of SendMessage calls. The beginning of the method is this:
Code:
 		public override void OnDoubleClick( Mobile from ) 
		{ 
			switch ( Utility.Random( 8 ) )
			{
				default:
				case  0: from.SendMessage( "IT IS CERTAIN" ); break;
We need to add to the method a check to limit how far a player can be from the item, say 2 tiles, and we need to make sure the player has line of sight to the item. This would go at the beginning of our method just before the switch statement.
Code:
 		public override void OnDoubleClick( Mobile from ) 
		{ 
			if ( from.InRange( this, 2 ) && from.CanSee( this ) )
			{
				switch ( Utility.Random( 8 ) )
				{
There are a few things to notice here. First is the keyword "this." The keyword this always refers to the specific instance of the Class we are working in. In other words the manifestation of this script. In this case this will mean the EightBall that has received the DoubleClick. Second, we use the InRange() and CanSee() methods of the Mobile Class. These methods and many more can be found in the RunUO Docs. Finally, notice the && operator; this is the "conditional-AND" which you will see and use often.

The AND operator (&) and the conditional-AND operator (&&) perform a boolean evaluation on two expressions. If and only if both expressions are considered to be true will the result be true. (x & y) is true only if both x AND y are both true. The conditional-AND operator is a little smarter than the regular AND operator however. If the first expression in a conditional-AND turns out to be false, the second expression is never evaluated, there is no need since the result now has to be false. This works to the scripters advantage in avoiding the dreaded null reference exception. More on this later.

Now, we have added an if statement with an opening brace to our method. Since any opening brace must have a closing brace, we still have work to do. At the end of our switch statement, after it's closing brace, we need to add a closing brace for our if statement. It wouldn't hurt to also add a message to provide a bit of feedback to the player when they are too far away. Here is the complete method at this point.
Code:
 		public override void OnDoubleClick( Mobile from ) 
		{ 
			if ( from.InRange( this, 2 ) && from.CanSee( this ) )
			{
				switch ( Utility.Random( 8 ) )
				{
					default:
					case  0: from.SendMessage( "IT IS CERTAIN" ); break;
					case  1: from.SendMessage( "WITHOUT A DOUBT" ); break;
					case  2: from.SendMessage( "MY REPLY IS NO" ); break;
					case  3: from.SendMessage( "ASK AGAIN LATER" ); break;
					case  4: from.SendMessage( "VERY DOUBTFUL" ); break;
					case  5: from.SendMessage( "CONCENTRATE AND ASK AGAIN" ); break;
					case  6: from.SendMessage( "DON'T COUNT ON IT" ); break;
					case  7: from.SendMessage( "YES" ); break;
				}
			}
			else 
			{ 
				from.SendLocalizedMessage( 500446 ); // That is too far away. 
			} 
		}
We have now used two different versions of the SendMessage method. Mobile.SendMessage() will send a system message to the player that Mobile references. Mobile.SendLocalizedMessage() will also send a system message to the player but it sends a text string from the cliloc files on the clients computer. The cliloc is the Client Localization files. There are at least eight separate files for each of eight languages that the UO client supports. You pass an integer to the SendLocalizedMessage() method and it in turn displays the appropriate message in the language of the client. It is recommended that you add the message text as a comment so you will know later what you are sending. You may find the text and index numbers using a cliloc viewer.

The method looks pretty good at this point, but what if the EightBall is in the players pack? We can add that test to the same line we test the range in, but the boolean operator must be different. In this case we want to know if the item is in the pack OR both in range and in sight. Fortunately there is an OR operator (|) and a conditional-OR operator (||). To check the pack, we will use another method from the Docs; this time from the Item Class. IsChildOf( from.Backpack ) will return a true if this item is anywhere in from's backpack. Our if statement now looks like this.
Code:
if ( IsChildOf( from.Backpack ) || from.InRange( this, 2 ) && from.CanSee( this ) )

In order to add a time delay to our EightBall, we are going to need two new variables. We will declare those variables at the Class level, meaning not inside any methods. If they were declared inside a method they would only exist for the duration of the method. We will use two time related variables; a DateTime which identifies a point in time (ex. 3:00 PM today) and a TimeSpan which identifies a length of time (ex. 15 minutes.) They will be declared at the beginning of our Class, however order is really not important.
Code:
 namespace Server.Items 
{ 
	public class EightBall : Item
	{ 
		private DateTime lastused = DateTime.Now;
		private TimeSpan delay = TimeSpan.FromSeconds( 3 );
The DateTime lastused was initialized with DateTime.Now which is always the time at that moment, in this case the moment the EightBall is created. The TimeSpan variable delay is set to 3 seconds using a method of the TimeSpan Class called FromSeconds().

Now we need to again look at the OnDoubleClick() method to add one more test. We will add delay plus lastused (adding a TimeSpan to a DateTime results in a DateTime and is similar to saying "15 minutes after 3 o'clock") then see if that time is later than it is now. If so, it has not been long enough since the last use so we will exit the entire method using a return statement. Otherwise we will update the value of lastused and continue with the rest of the method.
Code:
 		public override void OnDoubleClick( Mobile from ) 
		{ 
			if ( lastused + delay > DateTime.Now ) 
				return;
			else
				lastused = DateTime.Now;
Since the body of both the if statement and the else statement are each only one line I left out the curly braces. If they were any longer the braces would not be optional.

Here is the complete script at this point:
Code:
using System; 
using Server; 

namespace Server.Items 
{ 
	public class EightBall : Item
	{ 
		private DateTime lastused = DateTime.Now;
		private TimeSpan delay = TimeSpan.FromSeconds( 3 );

		[Constructable]
		public EightBall() : base( 0xE2F ) 
		{
			Weight = 1.0; 
			Name = "a magic eight ball"; 
		} 

		public EightBall( Serial serial ) : base( serial ) 
		{ 
		} 

		public override void Serialize( GenericWriter writer ) 
		{ 
			base.Serialize( writer ); 
			writer.Write( (int) 0 ); 
		} 
       
		public override void Deserialize(GenericReader reader) 
		{ 
			base.Deserialize( reader ); 
			int version = reader.ReadInt(); 
		}

		public override void OnDoubleClick( Mobile from ) 
		{ 
			if ( lastused + delay > DateTime.Now ) 
				return;
			else
				lastused = DateTime.Now;

			if ( IsChildOf( from.Backpack ) || from.InRange( this, 2 ) && from.CanSee( this ) )
			{
				switch ( Utility.Random( 8 ) )
				{
					default:
					case  0: from.SendMessage( "IT IS CERTAIN" ); break;
					case  1: from.SendMessage( "WITHOUT A DOUBT" ); break;
					case  2: from.SendMessage( "MY REPLY IS NO" ); break;
					case  3: from.SendMessage( "ASK AGAIN LATER" ); break;
					case  4: from.SendMessage( "VERY DOUBTFUL" ); break;
					case  5: from.SendMessage( "CONCENTRATE AND ASK AGAIN" ); break;
					case  6: from.SendMessage( "DON'T COUNT ON IT" ); break;
					case  7: from.SendMessage( "YES" ); break;
				}
			}
			else 
			{ 
				from.SendLocalizedMessage( 500446 ); // That is too far away. 
			} 
		}
	} 
}
 
Status
Not open for further replies.
Top