1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
4.8571429252625/5, 7 ratings

Scripting for Dummies: First Script, Creating an Item

Summary of the tutorial

This is the best intro "scripting for dummies" I have found yet. I did not write this, and do not know where I found it. It seems to be from RunUO, originally. It was written by "David", apparently a RunUO moderator at some time, back in 2004. Anyway I take no credit for this.

It is arranged in 3 parts. First it goes through the steps to create a basic item (a magic 8 ball). Second it adds some double click functionality. Then, third, it adds range checking and a time delay.

A very easy to understand intro to scripting, and he explains every step of the way. It must be simple because even I understood it :)

~Note~
If this is the wrong place for this, feel free to move it or delete it as needed.

1 The Magic Eight Ball

Scripting for Dummies: First Script, Creating an Item

Scripting a RunUO Item for Dummies.

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.


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.

}

}

}

}