|
||
|
|||||||
| Script Support Get support for modifying RunUO Scripts, or writing your own! |
![]() |
|
|
Thread Tools | Display Modes |
|
|
#1 (permalink) | |
|
Moderate
Join Date: Nov 2002
Location: USA
Posts: 6,869
|
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; Code:
using System;
using Server;
namespace Server.Items
{
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
{
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 )
{
}
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";
}
Code:
public EightBall( Serial serial ) : base( serial )
{
}
Code:
public override void Serialize( GenericWriter writer )
{
base.Serialize( writer );
writer.Write( (int) 0 );
}
Code:
public override void Deserialize(GenericReader reader)
{
base.Deserialize( reader );
int version = reader.ReadInt();
}
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();
}
}
}
Quote:
Code:
public override void OnDoubleClick( Mobile from )
{
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;
}
}
Code:
case 0: from.SendMessage( "IT IS CERTAIN" ); break; Code:
case 0:
{
from.SendMessage( "IT IS CERTAIN" );
break;
}
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;
}
}
}
}
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 Forum Moderator The RunUO.com Forum Moderator Team Forum Rules and Guidelines RunUO Forum Search Engine RunUO Online Documentaton Download RunUO ver 2.0 Search my Current Scripts Last edited by David; 11-24-2007 at 12:31 AM. |
|
|
|
|
|
#2 (permalink) |
|
Moderate
Join Date: Nov 2002
Location: USA
Posts: 6,869
|
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;
}
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 Forum Moderator The RunUO.com Forum Moderator Team Forum Rules and Guidelines RunUO Forum Search Engine RunUO Online Documentaton Download RunUO ver 2.0 Search my Current Scripts |
|
|
|
|
#3 (permalink) |
|
Moderate
Join Date: Nov 2002
Location: USA
Posts: 6,869
|
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;
Code:
public override void OnDoubleClick( Mobile from )
{
if ( from.InRange( this, 2 ) && from.CanSee( this ) )
{
switch ( Utility.Random( 8 ) )
{
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.
}
}
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 ) ) Code:
namespace Server.Items
{
public class EightBall : Item
{
private DateTime lastused = DateTime.Now;
private TimeSpan delay = TimeSpan.FromSeconds( 3 );
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;
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.
}
}
}
}
__________________
David Forum Moderator The RunUO.com Forum Moderator Team Forum Rules and Guidelines RunUO Forum Search Engine RunUO Online Documentaton Download RunUO ver 2.0 Search my Current Scripts Last edited by David; 11-24-2007 at 12:14 AM. |
|
|
![]() |
| Bookmarks |
| Currently Active Users Viewing This Thread: 1 (0 members and 1 guests) | |
| Thread Tools | |
| Display Modes | |
|
|