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 (text):
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 (text):
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 (text):
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 (text):
[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 (text):
[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 (text):
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 (text):
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 (text):
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 (text):
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 (text):
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 (text):
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 (text):
case 0: from.SendMessage( "IT IS CERTAIN" ); break;
is the same as this
Code (text):
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 (text):
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;
}
}
}
}
Hopefully you now have a good understanding of how a basic script is created, all credit goes to David from the RunUO forums for providing code snippets and text for the tutorial above.