Programming for CoffeeMud 5.3 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
OverviewThe purpose of this document is to assist those who wish to add custom Items, MOBs, Behaviors, Properties, or other objects to CoffeeMud. The reader should be familiar with Java programming, and should be experienced with writing and compiling Java classes. The object oriented notions of class inheritance and polymorphism, as well as the Java constructs of interfaces should be at least vaguely familiar to you before attempting to build classes for CoffeeMud. Also, it is expected that all of the ideas presented in the Archons Guide and Game Builders Guide are completely familiar. The difference between a GenItem and a StdItem, or a GenMob and a GenPostman will not be explained in this document. It is not expected that someone would wish to dive in and make wholesale changes to the CoffeeMud system right away, but is more likely wanting to fill in a functional gap in the system for their own needs. For this reason, this document is not organized as a comprehensive guide to programming CoffeeMud. Instead, it is designed to be a quick reference for those who wish to create the spot MOB, Behavior, Item, or Property for use on their maps. With this in mind then, let's start out with some brief development instructions, and then and in no particular order, discuss the several essential object types in CoffeeMud are presented.
Rebuilding CoffeeMudIn Microsoft Windows
In Unix
Introducing External ComponentsIf you perused the coffeemud.ini file as mentioned in the Installation Guide, you may have noticed and wondered about the section at the end which lists the default load paths for the several CoffeeMud objects. By default, the CoffeeMud engine dynamically loads the vast majority of its object code at boot time by referring to the paths
specified in the coffeemud.ini file. The value This default object boot paths may be removed or added-to using semicolon delimited paths, or even replaced with your own object boot directories. In fact, when adding objects to your CoffeeMud boot sequence, it is recommended that you place your objects in a separate directory path inside your CoffeeMud folder and add its path to the coffeemud.ini file under the proper setting. The order in which you place multiple paths in a single entry is also significant, as the CoffeeMud ClassLoader will load the files in the order in which they appear listed. For instance: MOBS=%DEFAULT%;/resources/examples/MyClass.class;/resources/otherclasses Will cause the default CoffeeMud versions of the mob classes to be loaded first, followed by MyClass.class, followed by all the class files in the resources/otherclasses folder in your CoffeeMud package. Also notice that the ClassLoader follows the rules of the CMFS described in the Archons Guide, meaning that the forward slash is always the proper path separator, and that no folder outside of your CoffeeMud package may be referenced. Also bear in mind that case is sensitive when naming Java class files, even in these boot paths. When writing these custom classes for your special object boot directory(s), it is important to keep a number of things in mind:
Complete Default Import Listimport com.planet_ink.coffee_mud.core.*; import com.planet_ink.coffee_mud.core.interfaces.*; import com.planet_ink.coffee_mud.Abilities.interfaces.*; import com.planet_ink.coffee_mud.Areas.interfaces.*; import com.planet_ink.coffee_mud.Behaviors.interfaces.*; import com.planet_ink.coffee_mud.CharClasses.interfaces.*; import com.planet_ink.coffee_mud.Commands.interfaces.*; import com.planet_ink.coffee_mud.Common.interfaces.*; import com.planet_ink.coffee_mud.Exits.interfaces.*; import com.planet_ink.coffee_mud.Items.interfaces.*; import com.planet_ink.coffee_mud.Locales.interfaces.*; import com.planet_ink.coffee_mud.MOBS.interfaces.*; import com.planet_ink.coffee_mud.Races.interfaces.*; import com.planet_ink.coffee_mud.Libraries.interfaces.*; import java.io.IOException; import java.util.*;
TextBefore we get started with objects, needs must the topic of text display be covered. Throughout the system you will see text being sent to the user. Since a mud is a text producing engine, this should be no great surprise. However, within that text you will often see different kinds of codes and tags which affect the output. For instance, consider the following lines: msg=CMClass.newMsg(mob,target,this,affectType,"<S-NAME> reach(es) for <T-NAMESELF>."); mob.location().show(mob,null,CMMsg.MSG_OK_ACTION,"<S-NAME> regain(s) <S-HIS-HER> feet."); Focusing only on the text for a moment, you will notice that special tags are used to designate a player name, or the name of the target of a spell. You will also notice that (s) and (es) is used to modify the proper form of a verb. These are key features of the CoffeeMud text engine. Here is a more complete list of available tags:
Occasionally, you will find color/font codes embedded in system strings. For instance:
msg.append("^!You are thirsty.^?\n\r");
These codes are as follows:
As you might have guessed, it is preferred that the system colors (the last 16 codes) be used sparingly, in favor of the more customizable codes above.
JavaScripting:JavaScript is an interpreted scripting language which is used in various parts of CoffeeMud. The CoffeeMud engine integrates the Rhino Javascript interpretor package from Mozilla in such places as the Scriptable behavior, the JRun command, the ClassLoader, the Quest engine, and the web server. In lieu of a complete write up on the syntax of this language, it is suggested that you read the documentation available from the authors of the interpretor here: http://www.mozilla.org/js/ and http://www.mozilla.org/rhino/. If you are familiar with writing Javascript for web browsers, there are several differences you will need to adjust to when using Javascript in CoffeeMud. A minor difference is that the Rhino interpretor requires that all variables be declared before use. A more important difference is that Javascript Strings are not the same as Java String objects, and that confusing them can lead to errors. To get around this problem, all of the implementations of Javascript in CoffeeMud, with the exception of the ClassLoader, Javascript in CoffeeMud, with the exception of the ClassLoader, provide a special method to convert Javascript strings into Java Strings before passing them to Java methods or objects which will require them. An example is below: var javascriptstring=' this is a javascript string '; var javastring=toJavaString(javascriptstring); // and now javastring is a real-live Java-compliant and Java-friendly string When writing Java classes in JavaScript, however, this method is only available through the CMLib object in the core package.
This is how you would access the var javascriptstring=' this is a javascript string '; var javastring=Packages.com.planet_ink.coffee_mud.core.CMLib.toJavaString(javascriptstring); // and now javastring is a real-live Java-compliant and Java-friendly string The most important difference between coding Javascript for CoffeeMud and for browsers is that there is no HTML DOM (Document Object Model), and therefore several of the libraries you are used to are probably missing, such as Math. For this reason, it is necessary for you to learn the CoffeeMud object packages in order to get access to useful data and useful libraries. And now you understand why the JavaScripting notes are kept in the Programming guide. :) To access the CoffeeMud object packages, you will need to make use of the Packages object to reference external packages. So
long as the imported objects are in your CoffeeMud classpath, they can be accessed and used. For instance, to use the CoffeeMud
var lib=Packages.com.planet_ink.coffee_mud.core.CMLib; // the above creates a reference to the CoffeeMud Library as a shortcut var value=lib.math().pow(4,2); // now we can access the math() library from our shortcut. Depending upon the context from which your script runs (the Scriptable behavior, JRun command, the Quest engine, or the
http/web server), certain other objects are made available to assist scripts in properly interacting with their environment. When
writing Java classes in JavaScript for the ClassLoader, however, you must always use the
In the Scriptable behavior, several methods are made available to access objects which are related to the event which
triggered the scripted code. These methods include The last piece of general information about JavaScript in CoffeeMud concerns writing Java classes for the CoffeeMud
ClassLoader. Any JavaScript file *.js included in the ClassLoader boot paths (see the previous section) will be loaded and
treated just like any Java compiled *.class file. The JavaScript file would be parsed, compiled, and loaded at boot time. For the
most part, writing Java Classes in JavaScript is extremely similar to writing Java Classes in Java. Base classes may be extended
(using the special CoffeeMud Writing your first JavaScriptBelow is an example of a Java Class written in JavaScript. Examples of Embedded JavaScript in CoffeeMud Virtual Pages (cmvp) web files can be found in the Web Server Guide. Examples of Embedded JavaScript in a Scriptable MOBPROG script can be found in the Scripting Guide. Our Java Class example is called GenLemming.js. It is a sample MOB class to demonstrate extending the GenMob class to create a type of modifiable mob for your maps. In this example, we add functionality to make all mobs in the world created from the GenLemming base suicidal. To use this class, save the code somewhere in our CoffeeMud folder under the name "GenLemming.js", and add an object path reference to it in the MOBS entry in your coffeemud.ini file, as described in section one.
//extends com.planet_ink.coffee_mud.MOBS.GenMob
function ID(){return "GenLemming";}
var lib=Packages.com.planet_ink.coffee_mud.core.CMLib;
The first lines of our class include the special //extends command which informs the CoffeeMud ClassLoader that this JavaScript class will extend GenMob, thereby inhereting all functionality of the maleable GenMob. The Lastly, we define the variable "lib" to act as a shortcut to the CoffeeMud core Libraries. Now, moving on; since we don't have the ability to write constructors in JavaScript, any initial fields we need to set
whenever a new instance of our class is created must be done in the CoffeeMud
function newInstance()
{
var lemm=this.super$newInstance();
lemm.setName("a generic lemming");
lemm.setDisplayText("a generic lemming is waiting to commit suicide");
return lemm;
}
There are several interesting points to make here. One is to notice that the function has no explicit return type, which is
part of the JavaScript standard of being "weakly typed". Also notice the syntax for calling the SuperClass version of the
And now we move on to overriding our first GenMob method, tick.
var countdown = 10;
function tick( host, tickID )
{
if( !this.amDead() )
{
countdown--;
if( countdown <= 0 )
{
lib.combat().postDeath( null, this, null );
countdown = 10;
}
}
return this.super$tick( host, tickID );
}
The For our GenLemming, we create a variable to count down the ticks from 10 to 0. When the countdown variable reaches 0, we call
the Now we'll get creative and implement a message previewer (see the Core Topic 1 below for more information on message previewing and handling). This method will be called when any event happens in the same room as the GenLemming mob. We will use this fact to look for, capture, and modify the message string which will inform the room of our impending death. Since it is the previewing method, it will be called BEFORE the activity actually takes place, giving us a chance to make our modifications before anyone actually sees the message strings.
function okMessage(host,msg)
{
if( ( msg.isSource( this ) )
&&( msg.isOthers( "DEATH" ) )
&&( msg.othersMessage() != null )
)
{
msg.setOthersMessage("<S-NAME> jumps off a cliff!!!");
}
return this.super$okMessage(host,msg);
}
In our message previewing method, we will check every message that comes our way, acting only if this particular GenLemming is
the source of the message, that he appears to be dying to others in the room
Core Topic 1: Getting the Message:CoffeeMud is essentially a distributed message passing and handling system, where the actions and events that occur in the system are represented as messages (Common.interfaces.CMMsg) which are then previewed, modified, cancelled, and/or reacted to by handlers. Understanding this idea is key to fully understanding how CoffeeMud really works, so let's take a second and peruse this concept in more detail. Messages in CoffeeMud, at least as we are talking about them here, always represent Events. Events such as a mob picking up an
item, swinging a sword at an opponent, taking damage from a fireball, or getting pricked by a poisonous needle. These events can
never actually occur in CoffeeMud unless a proper message is generated for them first. These messages, in the code, implement the
interface Messages are created at the moment that the event needs to occur. This moment can be triggered by the player entering a command into their telnet client and pressing Enter. It can also by triggered by the mindless algorithms which animate the mobs. Either way, when the moment has come, a message is created, and it looks like this:
CMMsg msg = CMClass.getMsg(mob, targetMOB, this,
CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invoke a spell at <T-NAME>s feet..^?",
CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invoke(s) a spell at your feet.^?",
CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invokes a spell at <T-NAME>s feet.^?"
);
The above message was taken from the code for the Grease spell, which calls the
The Source Code, Target Code, and Others Code is easily the most complicated aspect of a Message. For this reason, numerous pre-configured message codes have been created in the CMMsg interface, all of which begin with the characters MSG_. Although we will not go into the meaning of each of these messages (that will be left to the reader to search the code for instances of messages which use the codes, and learn from the context in which they are used), we can at least break down these codes so that they can be better understood. These coded integers all have two parts, the Major aspect (or the Major Code) and the Minor aspect (or the Minor Code). They
may be referenced off of an already constructed CMMsg object using such methods as The Major code is a series of significant bits in the integer, each of which gives some new meaning to the message. These bits are as follows:
The above masks can be quite confusing. It is best to examine the several MSG_ equates in the CMMsg interface to see how they are properly or improperly used. Remember a MSG_ equate is a completely constructed Code, complete with the appropriate Major and Minor aspects. The Minor Code represents the more specific activity being performed, and is a simple integer ranging from 0 (NO EFFECT) to 2047. The officially recognized Minor codes are exhaustively listed in the CMMsg interface, and all begin with the prefix TYP_. These types cover every sort of major event which occurs in the CoffeeMud engine, including getting items, casting spells, entering or leaving rooms, etc, etc..
CMMsg msg = CMClass.getMsg(attacker,target,weapon,CMMsg.MSG_WEAPONATTACK,
"<S-NAME>attack(s) <T-NAME>!");
The core.CMClass has many different getMsg signatures to make message construction quick and painless. The above is an example where only a single Code and a single message text are provided. In constructors where only one Code or message text field are provided, it is assumed that the code and message texts will be the same for source, target (if any) and others. CMMsg objects also have Message PreviewingOnce a Message has been constructed, it is time to actually put the message out into the system. There is a standard form for the sending of almost all messages. If the source of the message is a MOB called "SourceMOB", this standard form looks like this:
CMMsg msg = CMClass.getMsg(SourceMOB,TargetMOB,weapon,CMMsg.MSG_WEAPONATTACK,
"<S-NAME>attack(s) <T-NAME>!");
if(SourceMOB.location().okMessage(SourceMOB,msg))
{
SourceMOB.location().send(SourceMOB,msg);
}
The In the preview step, the room object will examine the message to see if there is anything which it might not like, wish to
modify, or wish to flag about the Message it has been handed. If the Room object does not like the message, it will return false.
Returning false from Inside the Message Flagging and ModificationMessage modification is very rare. When it happens though, it is done by calling one of the several Message flagging is somewhat less rare. Messages may be flagged when a combat strike is successful, or when a saving throw is
made against a spell Effect, or for any other reason the Message constructing code may wish. Flagging is done by calling the
aforementioned Message ExecutionOnce the The Of course, not every object in your game will handle and react to the Execution of every Message sent. Most of the time, a
given object will be ignoring the Message altogether. However, each object knows precisely which Messages are important for it,
and watch carefully for them in both their
Now that you are completely confused, it will make you at least a bit happier to know that Room objects have several short-cut methods for creating, previewing, and executing messages. They include the following:
public boolean show( MOB source,
Environmental target,
int allCode,
String allMessage
);
public boolean show( MOB source,
Environmental target,
Environmental tool,
int allCode,
String allMessage
);
public boolean show( MOB source,
Environmental target,
Environmental tool,
int srcCode,
int tarCode,
int othCode,
String allMessage
);
public boolean show( MOB source,
Environmental target,
Environmental tool,
int srcCode,
String srcMessage,
int tarCode,
String tarMessage,
int othCode,
String othMessage
);
public boolean show( MOB source,
Environmental target,
Environmental tool,
int allCode,
String srcMessage,
String tarMessage,
String othMessage
);
public boolean showOthers( MOB source,
Environmental target,
int allCode,
String allMessage
);
public boolean showOthers( MOB source,
Environmental target,
Environmental tool,
int allCode,
String allMessage
);
public boolean showSource( MOB source,
Environmental target,
int allCode,
String allMessage
);
public boolean showSource( MOB source,
Environmental target,
Environmental tool,
int allCode,
String allMessage
);
public void showHappens(int allCode, String allMessage);
public void showHappens( int allCode,
Environmental like,
String allMessage
);
The first methods (show) is very commonly used; it constructs a message with the given source and target (no tool), and with
the given Code and text message applying to source, target, and others. The All four of those methods will construct a CMMsg object, give the Message to the Room object for previewing ("okMessage"), and then, if the Message is not canceled, will call the Room "send" method for execution and return true. If the Message was canceled, false will be returned. Message TrailersThe final subject we will discuss in the area of Messages and Message handling regards another rare technique called Message
Trailer adding. Message Trailers are CMMsg objects which have been added to another CMMsg instance using the
Constructing and adding messages which act as message trailers can serve many purposes, but the most important of which is that the trailer messages only happen IF the host message also happens, and only happen AFTER the host message happens. This can be useful for the timing of subsequent messages which are dependent on others.
Core Topic 2: The State of Things:In most systems, it is typical for all of the data variables which describe a particular object to be coded directly inside that object. While this is also true in CoffeeMud, many important data fields, along with the appropriate "getter" and "setter" methods, are stored in separate special data-storage, or state objects. These data-storage objects provide access to numerous important properties for those objects. These storage/state objects are routinely copied and then the copies are modified by other objects which have a spacial relationship with them. For instance, a MOB object may copy one or more of its state objects, and then allow the local Room object, or his inventory Item objects, or spell Effect objects to modify the copy, The copied state object modifications are stacked on each other. Confused? Well, keep reading! Each instance of the several Environmental objects (MOBs, Items, Exits, Rooms) have a particular storage/state object called
their Environmental Stats. This object implements the Common.interfaces.EnvStats interface, and is typically an instance of the
Common.DefaultEnvStats class. Access to this state object is available through each core.interfaces.Environmental objects
EnvStats state object fields
Although most of these fields are better described in the Archon's Guide, there are two whose nature may not be readily apparent: the sensesMask and the disposition. These two integers are bitmaps. The value of each bit is defined by equates in the Common.interfaces.EnvStats interface. The equates which refer to the bits for sensesMask all begin with "CAN_", while the equates which refer to the bits for disposition all begin with "IS_". Now, as mentioned previously, all Environmental objects have two methods for accessing their EnvStats. One is
We must now introduce two other Environmental interface methods significant to this topic. One is the
The way in which the current EnvStats state object is modified by the
Here is an example: Gunker the Thief wears Full Plate Armor (Item), and has the Shield spell cast on him. His base Armor rating is 100. When he puts on the Plate Armor, the "recoverEnvStats" method is called on Gunker's MOB object. That method in turn calls the "affectEnvStats" method on the Plate Armor and the Shield spell Effect. Both of those methods improve the Armor rating on Gunker's MOB's current EnvStats by some number. Thus, Gunker becomes harder to hit in combat. Also, when Gunker picked up the Plate armor, the weight of the armor was added to Gunker's overall carried weight by increasing the weight value in Gunker's EnvStats object. I know, this is probably still confusing. Confusing or not, however, we still have to consider two other state objects, both of which are only available from the MOB object. One of which is the CharStats object, and the other of which is the CharState object. The CharStats objects are most closely analogous to the EnvStats objects. For instance, there are STAT_STRENGTH, STAT_INTELLIGENCE, STAT_DEXTERITY, STAT_CONSTITUTION, STAT_CHARISMA, STAT_WISDOM, STAT_GENDER, STAT_SAVE_PARALYSIS, STAT_SAVE_FIRE, STAT_SAVE_COLD, STAT_SAVE_WATER, STAT_SAVE_GAS, STAT_SAVE_MIND, STAT_SAVE_GENERAL, STAT_SAVE_JUSTICE, STAT_SAVE_ACID, STAT_SAVE_ELECTRIC, STAT_SAVE_POISON, STAT_SAVE_UNDEAD, STAT_SAVE_MAGIC, STAT_SAVE_DISEASE, STAT_SAVE_TRAPS, STAT_MAX_STRENGTH_ADJ, STAT_MAX_INTELLIGENCE_ADJ, STAT_MAX_DEXTERITY_ADJ, STAT_MAX_CONSTITUTION_ADJ, STAT_MAX_CHARISMA_ADJ, STAT_MAX_WISDOM_ADJ, STAT_AGE, STAT_SAVE_DETECTION, STAT_SAVE_OVERLOOKING. In addition to these equates defined and read through the Like the EnvStats above, those objects listed as able to modify the EnvStats current state object are the same objects which
are able to modify the CharStats state objects. Rereading the section on EnvStats will make clear how the CharStats objects are
modified in the same analogous manner, using repeated calls to The last state object to consider is the Common.interfaces.CharState objects on MOBs. The CharState object represents those fields which are constantly in flux: Hit Points, Mana, Movement, Hunger, Fatigue, and Thirst. Unlike EnvStats and CharStats, there are three CharState objects to consider for MOBs: the base CharState object (available
through The relationship between the above objects is as follows: The base CharState object represents the maximum values for the state variables BEFORE modification by magical armor or spells. The adjusted base CharState object (Max State) represents the maximum values for the state variables AFTER modification by magical armor or spells. The current CharState object (curState) represents the current hit points, mana points, etc available to the MOB. In the case of the CharState objects, adjustment by relevant objects is initiated by calling the MOBs
Once again, to understand one of them fully is to understand them all.
Core Topic 3: Tick TockOur last Core Topic will cover the ability of the mobs, items, exits, abilities, spell effects, behaviors, and other objects
to perform tasks on a regular, timed, basis. The tasks to be performed are always located within an method called These methods are called on a regular, timed basis whenever the object instance in question has been properly set up to do so, and at a defined frequency and interval. The "ticking" parameter is usually a reference to the object itself, or to the host object in the case of Behaviors. The "tickID" parameter describes what sort of regular timed event is occurring. These events are defined as equates in the core.interfaces.Tickable interface, and include IDs such as TICKID_MOB, TICKID_AREA, TICKID_EXIT_REOPEN and others. See the java file of that interface for more defined tickID values. Before we get into the methods by which an object instance are properly set up for regular calls to its
To sum up, MOBs have regular tick calls which they use to perform their own periodic tasks, as well as to allow their Behavior and spell effects to perform tasks. The other objects have circumstantial ticks in certain instances. Now, to add a new periodic call to the "tick" method on an Environmental object, one needs only to make a method call like this: CMClass.threads().startTickDown(theEnvObject,Tickable.MY_TICK_ID,TIME_TICK,NUM_TICKS); The second parameter is the tickID which will be used when the Now, the above version of CMClass.ThreadEngine().startTickDown(theEnvObject,Host.MY_TICK_ID,NUM_TICKS); Stopping any of these tick calls can be done by simply returning "false" from the tick method itself, or manually using the following: CMClass.ThreadEngine().deleteTick(theEnvObject,Host.MY_TICK_ID); You may also stop tick calls to an object by using the Environmental objects
Core Topic 4: Core LibrariesThe CoffeeMud engine contains numerous Java classes whose purpose is to perform much of the underlying game functionality. Some of these Java classes are Core classes, some are Library classes, and some are Common classes. Core classes are those classes found in the com.planet_ink.coffee_mud.core package. Like the interfaces, they may not be extended or overwritten without risking problems. The core classes, and their general purpose is:
You should check out the javadocs for those classes for more information on the core classes. * CMLib also refers to some of the sub-core classes, such as the Database access objects, and the Threading engine. While they are considered core, they are accessed through CMLib as if they were non-core libraries. Most core classes can also be accessed through CMLib methods, making it a one-stop shop. Now, non-core libraries, or Libraries proper, are located in the com.planet_ink.coffee_mud.Libraries package, and each one implements a unique interface from the com.planet_ink.coffee_mud.Libraries.interfaces package. This is unique, that each class in the Libraries package implements a unique interface all its own, but that is not the only unique thing about this package. Libraries are also singletons -- there is never more than 1 instance of each Library. Moreover, these singletons are all accessed from a single accessor class, CMLib, which we mentioned earlier. Here is a map of how the classes, interfaces, and CMLib methods are all mapped together:
Libraries are lastly unique in that it is almost useless to write new ones, unless you are doing the most serious additions and enhancements to your system. However, they are designed especially so they they can be extended and overridden. They have their own entry in the coffeemud.ini file called LIBRARY. By writing classes that extend the base CoffeeMud library classes, and overriding their methods, you can make changes to the most basic CoffeeMud algorithms, even at run-time! Libraries also have an entry in the coffeemud.ini file, LIBRARY, so that you can specify your custom extended versions of them after the %DEFAULT% string, thus allowing your changes to be loaded at boot-time. The last set of classes to discuss under this topic are neither Core class or Libraries, but form parts of the cores of other classes, including many of the Libraries. These are the Common classes. They are part of the com.planet_ink.coffee_mud.Common package. Like the Libraries, they each implement their own unique interface from the com.planet_ink.coffee_mud.Common.interfaces package. However, unlike the Libraries, numerous instances of each class will exist in your mud, and they are created from the core CMClass loader, much like MOBs, Items, Rooms, and so forth. The javadocs are also a good place to learn about these classes, but here is a brief list of them to wrap up our last Core Topic.
Core Topic 5: StatIn the uncoming sections, you will learn about the layout and design of each of the several major CoffeeMud class types. Sometimes, however, you may want to expand or extend the classes to include new or extraneous data. If so, this last core topic is for you. All of the classes which implement the Environmental interface (including Abilities, Exits, Areas, Rooms, MOBs, and Items) or the PlayerStats interface (Player MOBs) or several others, will include the following methods for accessing and modifying certain internal data fields:
public String[] getStatCodes();
public int getSaveStatIndex();
public String getStat(String code);
public void setStat(String code, String val);
The getStatCodes() method returns a string array of the "names" of the internal data fields which this class makes available to the outside world. These names can then be used in the getStat(String) method to retreive the string-rendered values of those data fields, and the setStat(String,String) method can be used to change them. This is a very versitile way of reading and writing the particular aspects of a data object, especially when you don't know for sure what kind of object it is (or you don't care). However, the reason this might interest you is because of the method we havn't mentioned yet: getSaveStatIndex(). The purpose of this method is quite simple: It returns the number of stat code names (from getStatCodes() ) that are already handled by the existing database code. This means that, for a stock CoffeeMud system, this method will return the same number as getStatCodes().length would. However, should you decide to add to the stat name strings defined by getStatCodes() and then extend getStat and setStat methods to manage your own internal variables, you have only to make sure your new name is at the end, and that getSaveStatIndex() stops short of that number, to make sure that CoffeeMud will go ahead and save your new data to the database (and restore it, of course). Exporting to cmare files and other features are also included this way. Here is an example:
public class MySpecialGenMob extends GenMOB
{
public String ID()
{
return "MySpecialGenMob;
}
private String myvariable="";
public String[] getStatCodes()
{
List L=java.util.Arrays.asList(super.getStatCodes());
L.add("MYVALUE");
return (String[])L.toArray();
}
public String getStat(String code)
{
if(code.equalsIgnoreCase("MYVALUE"))
{
return myvariable;
}
else
{
return super.getStat(code);
}
}
public void setStat(String code, String value)
{
if(code.equalsIgnoreCase("MYVALUE"))
{
myvariable=value;
}
else
{
super.setStat(code,value);
}
}
public int getSaveStatIndex()
{
return super.getStatCodes().length;
}
You'll notice above that the getStatCodes() method is extended to add a new stat code name "MYVALUE" which will appear at the end of the string list. The getStat and setStat methods are adjusted accordingly to support the new code. Lastly, the getSaveStatIndex method is overridden to return the length of the base classes codes instead of mine, that way the getSaveStatIndex for MySpecialGenMob will return one less than the getSaveStatIndex for GenMOB, the super class. And just by doing those simple things, we now have a new variable which the CoffeeMud engine will ensure is written to the database. However, this capability is limited to the following types of objects: generic mobs, generic items, playerstats, or any exits, rooms, or area classes. Since players are technically implemented as StdMOB objects, you will need to modify the appropriate methods in the DefaultPlayerStats object to achieve the same effect for players. To add a new savable stat code to those already defined in DefaultPlayerStats, the first thing we'd do is add a new variable name (CODE name) to the end of the CODES list above. Make sure you don't insert it anywhere, as the order is significant. Add it to the end, in uppercase, as so:
protected static String[] CODES={"CLASS","FRIENDS","IGNORE","TITLES",
[...this continues...]
"ACCTEXPIRATION","INTRODUCTIONS", "MYNEWVARIABLE"};
Next, you would add the define a variable to hold your new data, and provide the ability to GET the data to the getStat method:
private String myNewString="";
public String getStat(String code)
{
switch(getCodeNum(code))
{
case 0: return ID();
case 1: return getPrivateList(getFriends());
[...this continues...]
case 18: return ""+accountExpiration;
case 19: return getPrivateList(introductions);
case 20: return myNewString;
}
return "";
}
Next, you would add the ability to SET the data to the setStat method:
public void setStat(String code, String val)
{
switch(getCodeNum(code))
{
case 0:
break;
case 1:
friends=getHashFrom(CMLib.xml().returnXMLValue(val,"FRIENDS"));
break;
[...this continues...]
case 19:
introductions=getHashFrom(CMLib.xml().returnXMLValue(val,"INTROS"));
break;
case 20: myNewString=val;
break;
}
}
And the last step would be to modify the getSaveStatIndex method by HIDING our new variable from its count of codes by subtracting one from the new CODES count in order to reach the old one:
public int getSaveStatIndex()
{
return super.getStatCodes().length - 1;
}
This last step is a bit hard to understand, but remember that: the CODES from 0 to getSaveStatIndex() are all already handled by the internal codebase. Since we added a new code to the list, this method, if left unchanged, would return a number that is ONE larger than it used to be. However, that would be implying that our new variable is also already handled by the codebase, which isn't true. By subtracting one, we tell the CoffeeMud engine that 0 to getSaveStatIndex()-1 is handled by the system, but any codes beyond that need special treatment, which is true.
CommandsCommands are exactly what they sound like: LOOK, QUIT, KILL, GET, and all the other things you type into the mud are handled by CoffeeMud Commands. A Custom Command may or may not belong to any particular package, though it is important that the
public class DoNothing extends com.planet_ink.coffee_mud.Commands.StdCommand
{
public DoNothing(){}
private String[] access={ "DONOTHING" };
public String[] getAccessWords(){ return access; }
All Commands should extend StdCommand for conformity's sake, though it is not required so long as your class implements the com.planet_ink.coffee_mud.core.interfaces.Command interface. In our example above, we have an empty constructor, but we do define some access words. Access words are what you think they are: the words which, when typed, allow the users to activate the command. We define a string array containing one such access word in this case, and then define our Command interface method getAccessWords() to return that array. Our String array may contain as many strings as you would need to provide sufficient words to activate this command.
public double actionsCost(MOB mob, Vector cmds){ return 1.0; }
public double combatActionsCost(MOB mob, Vector cmds){ return 1.0; }
public boolean canBeOrdered(){ return true; }
The next two methods, The
public boolean securityCheck( MOB mob )
{
return CMSecurity.isAllowed( mob, mob.location(), "IMMORT");
}
And speaking of security, this method returns whether or not the given mob may even have access to this command. If the
In the above example, we call the
public boolean preExecute(MOB mob, Vector commands, int secondsElapsed, double actionsRemaining)
throws java.io.IOException;
{
if( secondsElapsed == 0 )
{
mob.tell( "You are preparing to do nothing." );
}
if( secondsElapsed == 3 )
{
mob.tell( "You almost ready to do nothing." );
}
}
The
public boolean execute( MOB mob, Vector commands )
throws java.io.IOException
{
String parameters = CMParms.combine( commands, 1 );
if( parameters.length() == 0 )
{
mob.tell( "Nothing done." );
}
else
{
mob.tell( "Nothing, not even '" + parameters + "', done." );
}
return false;
}
Our last method is where the command actually does its work. The mob given would be the MOB object trying to execute this command, while commands is a Vector of parameters. The parameter commands is never null, and by convention is a Vector of strings starting with the access word used to execute the command. For instance, if the user entered:
donothing never "and always" never
The commands Vector would be size 4, and contain "donothing", "never", "and always", and "never" respectively. In the case of this command, we use one of the String utilities to recombine the last 3 parameters back into one string "never and always never", and then issue a message to the mob depending upon whether there were any parameters at all. Since this command requires a command word to access it, it is reasonable to assume that the 0th element in the commands vector is the word "donothing", which means we can safely ignore it.
MOBsMOBs, or "Moveable OBjects", are the creatures and characters which the players fight. In CoffeeMud, they are among the simpler objects to code. This is not because they are uncomplex. In fact, they are MOST complex. However, this complexity comes due to the myriad of Items, Behaviors, Properties, and Abilities that are added to them. Short of these numerous additions, a MOB by himself is rather simple! This simplicity is important however, and should be carefully considered before you run off to create new MOBs. If you are
creating a new MOB because you want a creature to have some new kind of ability, then are you sure it is not a new Ability you
want to write? If the new MOBs behavior is complex and unique, are you sure it's not a new Behavior you wish to code? Otherwise,
the best reasons to be coding MOBs are actually three: because you have a particular kind of monster that is used prolifically in
your world, and you want to save memory by coding him as a special mob that extends StdMOB, or because you want to code special
player capabilities by creating your own class that both extends StdMOB and has an So, if you are sure this is what you want to do, carry on! The directory for your custom coded MOB objects should be specified using the "MOBS" entry in the coffeemud.ini file. See the section above on Rebuilding CoffeeMud for more information on this feature. Coding a new MOBA Custom MOB may or may not belong to any particular package, though it is important that the ID() of the MOB be unique in the system. A custom MOB imports the same packages mentioned in the first section of this document under Complete Default Import List as well as (in this case) com.planet_ink.coffee_mud.MOBS.StdMOB because our sample mob extends it. A MOB class must extend either StdMOB or GenMob, StdShopKeeper or GenShopKeeper, StdRideable or GenRideable depending on the
basic capabilities, and customizability you would like. Although Generic objects are more customizable at run-time, they also
take a long time for the system to load and build, and take up a lot of database disk space, and more memory. For this reason,
using Standard instead of Generic wherever possible is always good. Another reason for extending StdMOB is because all players in
the game use the "StdMOB" class as a basis, which means that special player fields and capabilities can be coded by both
extending the StdMOB class, and also giving your custom class an As was stated, each unique MOB must also have a custom
public class MyNewMOB extends com.planet_ink.coffee_mud.MOBS.StdMOB
{
public String ID(){ return "MyNewMOB";}
All of your customizing will be done inside the constructor: name, displayText, description, etc, etc.
public MyNewMOB()
{
super();
setName( "a new mob" );
setDescription( "It`s furry with 2 legs" );
setDisplayText( "My new mob is standing here." );
Factions.setAlignment( this, Faction.ALIGN_NEUTRAL );
setMoney( 0 );
setWimpHitPoint( 2 );
baseEnvStats().setDamage( 4 );
baseEnvStats().setAbility( 0 );
baseEnvStats().setLevel( 1 );
baseEnvStats().setArmor( 30 );
baseEnvStats().setSpeed( 1.0 );
baseEnvStats().setAttackAdjustment( 30 );
baseEnvStats().setWeight( 85 );
baseEnvStats().setSensesMask( EnvStats.CAN_SEE_DARK|EnvStats.CAN_SEE_INFRARED );
baseEnvStats().setDisposition( EnvStats.IS_FLYING );
baseCharStats().setCurrentClass( CMClass.getCharClass( "Fighter" ) );
baseCharStats().setMyRace( CMClass.getRace( "Dog" ) );
baseCharStats().getMyRace().startRacing( this, false );
baseCharStats().setStat( CharStats.STAT_GENDER, (int)'F' );
baseCharStats().setStat( CharStats.STAT_STRENGTH, 18 );
baseCharStats().setStat( CharStats.STAT_INTELLIGENCE, 14 );
baseCharStats().setStat( CharStats.STAT_WISDOM, 13 );
baseCharStats().setStat( CharStats.STAT_DEXTERITY, 15 );
baseCharStats().setStat( CharStats.STAT_CONSTITUTION, 12 );
baseCharStats().setStat( CharStats.STAT_CHARISMA, 13 );
baseCharStats().setStat( CharStats.STAT_SAVE_COLD, 50 );
baseState.setHitPoints( CMLib.dice().roll( baseEnvStats().level(), 20, 20 ) );
baseState.setMana( CMLib.dice().roll( baseEnvStats().level(), 50, 100 ) );
recoverMaxState();
resetToMaxState();
recoverEnvStats();
recoverCharStats();
}
You can see here that the basic stats have been filled out, from level to attack speed, alignment and weight. For numeric values, higher is always better, except for Armor, which is always best low, and comes down from 100. You'll notice above that two commands are required to set the Race of the creature. Also, you should realize that the numerous saving throws, and senses as well as dispositions (sneaking, hiding) are not represented above, but can easily be added using the format shown. It is very important to note the last four commands. These commands "reset" the MOB, and "implement" the scores which are
given in the several areas. Now, suppose we wanted to add an Effect or Behavior or Ability to your MOB. The proper place for such a statement would be in the same above constructor, among the other commands. Preferably before the several "recover" commands, but after the several stat definitions. Of course, all of this is unnecessary for a new GenMOB object.
addNonUninvokableEffect( CMClass.getAbility( "Fighter_Berzerk" ) );
Ability A = CMClass.getAbility( "Prop_Resistance" );
if( A != null )
{
A.setMiscText( "Fire 200%" );
addNonUninvokableEffect( A );
}
addAbility( CMClass.getAbility( "Poison" ) );
addBehavior( CMClass.getBehavior( "MudChat" ) );
addBehavior( CMClass.getBehavior( "Mobile" ) );
addBehavior( CMClass.getBehavior( "CombatAbilities" ) );
The commands above will make the MOB permanently Berzerk, gives it the ability to Poison folks while in combat, allows the MOB to Chat with other players, and to walk around in its area. The last behavior gives the MOB the wisdom to use its Poison ability while in combat. If your MOB extends StdShopKeeper, you will need to add your inventory manually through the
setWhatIsSold( ShopKeeper.ONLYBASEINVENTORY );
Weapon sword = (Weapon)CMClass.getWeapon( "Longsword" );
getShop().addStoreInventory( sword, 35, -1, this );
Armor mail = (Armor)CMClass.getArmor( "FullPlate" );
getShop().addStoreInventory( mail, 35, -1, this );
Item waterskin = CMClass.getItem( "Waterskin" );
getShop().addStoreInventory( waterskin, 35, -1, this );
You'll recall from the Archon's Guide that there are many different types of ShopKeepers, including trainers, pet sellers, weaponsmiths, and others. StdRideable MOBs will require a few other settings as well!
setRideBasis( Rideable.RIDEABLE_LAND );
setMobCapacity( 2 );
The last thing is to give the MOB equipment, armor, and weapons. The following commands will do the trick!
Weapon sword = (Weapon)CMClass.getWeapon( "Longsword" );
addInventory( sword );
sword.wearIfPossible( this );
Armor mail = (Armor)CMClass.getArmor( "FullPlate" );
addInventory( mail );
mail.wearIfPossible( this );
Item sack = (Item)CMClass.getItem( "StdContainer" );
addInventory( sack );
Item waterskin = (Item)CMClass.getItem( "Waterskin" );
addInventory( waterskin );
waterskin.setContainer( sack );
And that's all there is to creating a new standard MOB. Easy, huh? Well, obviously, the real complexity of MOBs comes when the Behaviors and Abilities are programmed, but that is not covered here, of course. There is also the advanced topic of extending the capabilit | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||