#charset "us-ascii"
#include "advlite.h"
/*
* RULES EXTENSION by Eric Eve July 2014
*
* The rules.t extension allows Inform7-style rules and rulebooks to be
* defined in a TADS 3 game.
*/
/*
* A RuleBook is a container for one or more rules. Calling the follow()
* method of a RuleBook causes each of its contained rules to be executed in
* turn until one returns a non-null value. That value is then returned to the
* caller of the RuleBook. [DEFINED IN RULES EXTENSION]
*/
class RuleBook: PreinitObject
/* A list of rules contained in this rulebook */
contents = []
/*
* The actor to use for comparison with the who property for rules in this
* RuleBook. The default is gPlayerChar, but for some RuleBooks gActor may
* be more appropriate.
*/
actor = gPlayerChar
/* Add a rule to the contents of this rulebook [RULES EXTENSION] */
addToContents(ru)
{
contents += ru;
}
/* Remove a rule from the contents of this rulebook [RULES EXTENSION] */
removeFromContents(ru)
{
contents -= ru;
}
/*
* Follow this Rule. This is the method game code will normally call to
* make use of this RuleBook. Each of our rules will be tested to see if
* it matches its conditions; we then run through those of our rules that
* match their rules in order of precedence until one returns a non-null
* value, which we then in turn return to our caller. If no rule returns a
* non-null value we return our own default value, which is normally nil.
*
* This method can be called with as many arguments as the game code finds
* useful, or with none at all. Our arguments will then be passed on to
* each Rule that is called. The first argument will also be stored in our
* matchObj property, which our Rules can compare with their own matchObj
* condition to see if they match. This allows game code to, for example,
* run a RuleBook related to some object that isn't one of the objects
* directly involved in the current action. [RULES EXTENSION]
*/
follow([args])
{
/*
* If we have any arguments at all, store the first one in our
* matchObj property for comparison with our Rules' matchObj
* conditions.
*/
if(args.length > 0)
matchObj = args[1];
/* Carry out any custom initialization required. */
initBook(args...);
/*
* Make sure that all the Rules in our contents list know that we're
* the RuleBook that's currently calling them. This is necessary in
* case the same Rule is associated with more than one RuleBook.
*/
contents.forEach({ r: r.setRulebook(self) });
/* Extract the subset of Rules that match their conditions. */
local validRules = contents.subset({r: r.matchConditions});
/* Sort the matching rules in descending order of precedence. */
validRules = validRules.sort(SortDesc, {a, b: a.compareTo(b)} );
/*
* Go through each of the matching rules in turn, calling its follow
* method. If any rule returns a non-null value, stop running through
* the rules and return that value to our caller.
*/
foreach(local ru in validRules)
{
IfDebug(rules,
"[Executing rule: <<valToSym(ru)>> in rulebook
<<valToSym(self)>>]\n" );
local res = ru.follow(args...);
if(res != contValue)
return res;
}
/*
* If no rule returned a non-null value, return our own defaultVal to
* our caller.
*/
return defaultVal;
}
/*
* contValue (continue value) is the value that a Rule in this RuleBook
* needs to return to avoid the RuleBook stopping at that Rule (rather
* than going on to consider more Rules). By default this is null, which
* means by default a Rule that does not explicitly return a value (and so
* effectivelt returns nil) will stop the RuleBook. If you want the
* default behaviour for this RuleBook to be not for Rules to stop the
* book, then override this to nil. [RULES EXTENSION]
*/
contValue = null
/*
* The default value to return to our caller. By default this is the same
* as our contValue , to make it easy to test whether we any rule returned
* a non-null value. By default a rule that does something will return
* nil, so if no rule does anything we want to return a different value.
* By making the defaultValue the same as the contValue, we ensure that we
* can tell our caller that no rule was executed (if that is indeed the
* case). [RULES EXTENSION]
*/
defaultVal = contValue
/*
* The value our associated rules use by default to stop this RuleBook
* considering any further rules (when a Rule uses the stop macro). By
* default we use a value of true. [RULES EXTENSION]
*/
stopValue = true
/*
* Game code can use this method to initialize the values of custom
* RuleBook properties at the start of the processing of following a
* RuleBook. [RULES EXTENSION]
*/
initBook([args]) { }
/*
* The object (or any other value) to be matched by our Rule's matchObj
* conditions if they have any. This property is set by our follow()
* method (from its first argument) and so should not normally be directly
* changed from game code. [RULES EXTENSION]
*/
matchObj = nil
;
/* Preinitializer for Rules. [DEFINED IN RULES EXTENSION]*/
rulePreinit:PreinitObject
execute()
{
/* Initialize all the Rules in the game. */
for(local ru = firstObj(Rule); ru != nil; ru = nextObj(ru, Rule))
{
ru.initializeRule();
}
}
;
/*
* A Rule is an object that defines a set of conditions that need to be met
* for it to be executed when its RuleBook is run and a method that's executed
* when its conditions are met. A Rule can be associated with one or more
* RuleBooks; it starts out in the RuleBook with which it is associated via
* its + property (i.e. its location). [DEFINED IN RULES EXTENSION]
*/
class Rule: object
/*
* Our location is the RuleBook with which we start out being associated.
* Normally this will be defined by locating a Rule inside its RukeBook
* using the + notation. [RULES EXTENSION]
*/
location = nil
/*
* The rulebook that's currently considering us. Normally this will be our
* location, but it could be a different RuleBook if we belong to one.
* Note that this property is automatically set by the library and so it
* should never need to be altered by game code. [RULES EXTENSION]
*/
rulebook = location
/*
* Set our current rulebook to r. Note that this method is normally called
* by the Rulebook that's running us, and shouldn't normally be used by
* game code. [RULES EXTENSION]
*/
setRulebook(r) { rulebook = r; }
/* A list of all the rulebooks this rule is currently associated with. [RULES EXTENSION] */
rulebooks = []
/*
* Initialize this Rule by adding it to the contents list of its location
* and calculating its specificity (i.e. how specific its conditions are)
* [RULES EXTENSION]
*/
initializeRule()
{
if(location)
{
location.addToContents(self);
rulebooks += location;
}
specificity = calcSpecficity();
}
/*
* Do whatever this Rule needs to do when its conditions are met. This
* method will need to be defined on each individual Rule in game code.
* [RULES EXTENSION]
*/
follow([args])
{
}
/*
* The priority of this Rule. This can be used to alter the order in which
* this Rule is considered in its RuleBook. If two Rules have different
* priorities they will be run in priority order, highest priority first.
* The default value is 100. [RULES EXTENSION]
*/
priority = 100
/*
* Where two Rules have the same priority, the one with the more specific
* conditions is taken first. The specificity property holds a measure of
* the Rule's specificity which is calculated by the calcSpecificity()
* method at PreInit. [RULES EXTENSION]
*/
specificity = nil
/*
* Return true if this Rule should always execute after other (despite all
* other ranking criteria). By default we return true if and only if other
* is in our execAfter list. [RULES EXTENSION]
*/
runAfter(other)
{
return valToList(execAfter).indexOf(other) != nil;
}
/*
* A list of Rules this Rule should specifically run after; this overrides
* all other ranking. [RULES EXTENSION]
*/
execAfter = []
/*
* Return true if this Rule should always execute before other (despite all
* other ranking criteria). By default we return true if and only if other
* is in our execBefore list. [RULES EXTENSION]
*/
runBefore(other)
{
return valToList(execBefore).indexOf(other) != nil;
}
/*
* A list of Rules this Rule should specifically run before; this
* overrides all other ranking except for runAfter/execAfter.
* [RULES EXTENSION]
*/
execBefore = []
/*
* A Rule is normally active (that is it will normally be considered when
* a RuleBook is being followed) but it can be temporarily disabled by
* setting its isActive property to nil. [RULES EXTENSION]
*/
isActive = true
/* Make this Rule active [RULES EXTENSION] */
activate() { isActive = true; }
/* Make this Rule inactive [RULES EXTENSION] */
deactivate() { isActive = nil; }
/*
* Calculate the specificity of this Rule. The principles are (a) Rules
* that specify more conditions are more specific than Rule that specify
* fewer condition; (b) conditions involving specific objects are more
* specific that those relating to classes and (c) Rooms are more specific
* than Regions in a where condition. [RULES EXTENSION]
*/
calcSpecficity()
{
local p = 0;
/* a 'when' has priority over no 'when' */
if(propDefined(&when))
p += 10;
/*
* a 'where' has priority over no 'where', and a where property that
* specifies a Room is more specific than one that specifies a Region.
*/
if(propDefined(&where))
p += valToList(where).indexWhich({r: r.ofKind(Room)}) ? 10 : 5;
/* a 'during' has priority over no 'during' */
if(propDefined(&during))
p += 10;
/* a 'who' has priority over no 'who' */
if(propDefined(&who))
p += 10;
/* an 'action' has prioity over no 'action' */
if(propDefined(&action))
{
p += 5;
}
/*
* A Rule that refers to command objects (direct, indirect and
* accessory) is more specific than one that does not, and particular
* objects are more specific than classes.
*/
for(local prop in [&dobj, &iobj, &aobj])
{
if(propDefined(prop))
{
p += valToList(self.(prop)).indexWhich( {x: !x.isClass()} ) !=
nil ? 2 : 1;
}
}
/*
* If matchObj is defined, then what we do with it depends on the type
* of value(s) it contains.
*/
if(propDefined(&matchObj))
{
switch(propType(&matchObj))
{
/* If matchObj is nil, then ignore it. */
case TypeNil:
break;
/*
* If it's an object, increase our specificity by 10, unless
* it's a class, in which case only increase it by 5 (a class
* is less specific than an object).
*/
case TypeObject:
p += (matchObj.isClass ? 5 : 10);
break;
/*
* If it's a list, check what kind of values the list
* contains.
*/
case TypeList:
/*
* If the first item in the list is an object, assume the
* whole list contains objects, then increase our specificity
* by 10 if any of those objects is not a class, and by 5
* otherwise (classes are less specific than objects).
*/
if(matchObj.length > 0 && dataType(matchObj[1]) == TypeObject)
{
if(matchObj.indexWhich({o: !o.isClass}))
p += 10;
else
p += 5;
}
/*
* Otherwise, if it's some other kind of value, simply
* increase our specificity by 10.
*/
else
p += 10;
break;
/*
* For any other kind of value, simply increase our
* specificity by 10.
*/
default:
p += 10;
break;
}
}
/* Check if our present property is defined. */
if(propDefined(&present))
{
if(dataType(&present) == TypeObject && present.isClass)
p += 5;
else
p += 10;
}
/* Return the result of the calculation. */
return p;
}
/*
* Get the processing priority sorting order relative to another
* Rule. [RULES EXTENSION]
*/
compareTo(other)
{
/*
* If we specifically want this Rule to run after other, rank us after
* other
*/
if(runAfter(other))
return -1;
/*
* If we specifically want this Rule to run before other, rank us
* before other.
*/
if(runBefore(other))
return 1;
/*
* If the two Rules have different priorities, rank them in order of
* priority.
*/
if(priority != other.priority)
return priority > other.priority ? 1 : -1;
/*
* Otherwise, if they have different speficifities, rank them in order
* of specificity.
*/
if(specificity >= other.specificity)
return specificity > other.specificity ? 1 : -1;
/*
* Failing all else, go by the relative location of the source
* code definitions: the definition that appears later in the
* source code takes precedence. If the two are defined in
* different modules, the one in the later module takes
* precedence.
*/
if (sourceTextGroup != other.sourceTextGroup)
return sourceTextGroup.sourceTextGroupOrder
- other.sourceTextGroup.sourceTextGroupOrder;
/* they're in the same module, so the later one takes precedence */
return sourceTextOrder - other.sourceTextOrder;
}
/* Check whether a Rule matches its where, when, who and during conditions. [RULES EXTENSION] */
matchConditions()
{
/* If this Rule is currently inactive it can't match any conditions. */
if(!isActive)
return nil;
/* first check the where condition, if there is one. */
if(propDefined(&where))
{
local whereLst = valToList(where);
/*
* if we can't match any item in the where list to the player
* char's current location, we don't meet the where condition, so
* return nil
*/
if(whereLst.indexWhich( {loc: gActor.isIn(loc)}) == nil)
return nil;
}
/*
* Interpret 'when' as simply a routine that returns true or nil
* aocording to some condition defined by the author; so we simply
* test whether when returns nil if the property is defined.
*/
if(propDefined(&when) && when() == nil)
return nil;
/* check the who condition, if there is one. */
if(propDefined(&who))
{
local whoLst = valToList(who);
/*
* If we can't match any item in the who list to the current
* actor, we don't meet the who condition, so return nil
*/
if(whoLst.indexOf(gActor) == nil)
return nil;
}
/*
* if we're using the scene manager and a during condition is
* specified, test whether the scene is currently happening.
*/
if(defined(sceneManager) && propDefined(&during))
{
local duringList = valToList(during);
if(duringList.indexWhich({s: s.isHappening}) == nil)
return nil;
}
/*
* If we've specified an action to match, test whether gAction (the
* current action) matches it.
*/
if(propDefined(&action)
&& valToList(action).indexWhich({a: gAction.ofKind(a)}) == nil)
return nil;
/*
* If we've specified a dobj, iobj and/or aobj to match, test whether
* they match the direct object, indirect object and/or accessory
* object of the current action (provided there is one, which there
* may not be at startup).
*/
if(gAction)
{
for(local objs in [[&dobj, gDobj], [&iobj, gIobj], [&aobj, gAobj]])
{
local prop = objs[1];
local obj = objs[2];
if(propDefined(prop)
&& valToList(self.(prop)).indexWhich({o: obj.ofKind(o)}) == nil)
return nil;
}
}
/*
* If we have a matchObj defined, test whether it matches our current
* rulebook's matchjObj.
*/
if(propDefined(&matchObj))
{
local mList = valToList(matchObj);
local mo = rulebook.matchObj;
if(mList.length > 0)
{
/*
* If we want to match an object (or class), test whether the
* rulebook's match obj is of the appropriate kind. Since an
* object is always of its own kind, this tests either whether
* the rulebook's matchObj appears in our matchObj list or
* whether it belongs to one of the classes in our matchObh
* list.
*/
if(dataType(mList[1]) == TypeObject)
{
if(mList.indexWhich({o: mo.ofKind(o) } ) == nil)
return nil;
}
/*
* Otherwise, if we're not testing for an object, simply test
* whether the value of our rulebook's matchObj is equal to
* anyhting in our matchObj property.
*/
else if(mList.indexOf(mo) == nil)
return nil;
}
}
/*
* If present is defined, check whether at least one of the items in
* the list is in the same room, or can be sensed.
*/
if(propDefined(&present))
{
local pList = valToList(present);
/*
* First test for the special case that the present property
* specifies a single class. In which case whether anything in the
* location of actor matches that class.
*/
if(pList.length == 1 && dataType(pList[1]) == TypeObject &&
pList[1].isClass())
{
if(actor.getOutermostRoom.allContents.indexWhich(
{o: o.ofKind(present) }) == nil)
return nil;
}
/*
* Check whether the first item in list is a property pointer. If
* it is we want to use it as a property of the Q object to test
* for a sense connection.
*/
else if(pList.length > 0 && dataType(pList[1]) == TypeProp)
{
/* The first item in the list is a property pointer. */
local prop = pList[1];
/*
* Reduce the list to its remaining elements, which should all
* be objects.
*/
pList = pList.sublist(2);
/*
* If no item in pList has a sense path from the actor via the
* prop property, we don't match.
*/
if(pList.indexWhich({o: Q.(prop)(actor, o) }) == nil)
return nil;
}
/*
* Otherwise, simply test for the presence of one of the objects
* in the actor's room.
*/
else
{
local loc = actor.getOutermostRoom();
if(pList.indexWhich({o: o.isIn(loc)}) == nil)
return nil;
}
}
/*
* If we haven't failed any of the conditions, we're okay to match, so
* return true.
*/
return true;
}
/* Add this rule to another rulebook [RULES EXTENSION] */
addTo(rb)
{
if(rb && rb.ofKind(RuleBook))
{
rb.addToContents(self);
rulebooks += rb;
}
}
/* Remove this rule from a rulebook [RULES EXTENSION] */
removeFrom(rb)
{
if(rb && rb.ofKind(RuleBook))
{
rb.removeFromContents(self);
rulebooks -= rb;
if(location == rb)
location = nil;
}
}
/*
* Move this rule to another rulebook, removing it from all its current
* rulebooks. If rb is nil, simply remove this Rule from its current
* rulebooks. [RULES EXTENSION]
*/
moveInto(rb)
{
/*
* If rb is actually a RuleBook, add us to its contents and make rb
* our current location.
*/
if(rb && rb.ofKind(RuleBook))
{
rb.addToContents(self);
location = rb;
}
/*
* Remove us from the contents of all our previous rulebook
*/
rulebooks.forEach({r: r.removeFromContents(self)});
/* Add our new RuleBooks to our list of rulebooks */
if(rb)
rulebooks += rb;
/* Make rb our currently active rulebook */
rulebook = rb;
}
/*
* The value this rule should return when the stop macro is used at the
* end of its follow method. By default we use our rulebook's stopValue.
* [RULES EXTENSION]
*/
stopValue = (rulebook.stopValue)
/*
* The actor to use to compare with the who property of this Rule. This
* will normally be gPlayerChar, but the value of this property is taken
* from our RuleBook's actor property. [RULES EXTENSION]
*/
actor = (rulebook == nil ? gPlayerChar : rulebook.actor)
/* ------------------------------------------------------------------- */
/*
* One or more of the following properties, if defined, determine what
* conditions this Rule needs to match in order to be executed.
*/
/*
* A Room or Region, or a list of Rooms and/or Regions in which our actor
* (usually either gActor or gPlayerChar - the latter by default - must be
* for this Rule to match. [RULES EXTENSION]
*/
// where = []
/*
* A condition that must hold (or a method returning a Boolean value to
* determine whether or not appropriate conditions hold) for this Rule to
* match. This is only needed if none of the other properties in this
* section provide a way of speficifying the required conditions.
*/
// when() {}
/*
* An actor, or a list of actors, one of whom must be performing the
* current action for this Rule to match. [RULES EXTENSION]
*/
// who = []
/*
* A Scene, or a list of Scenes, one of which much be currently happening
* for this Rule to match. [RULES EXTENSION]
*/
// during = []
/*
* An action, or a list of Actions, one of which (e.g. Take or Jump) must
* be the current action in order for this Rule to match. [RULES EXTENSION]
*/
// action = []
/*
* An object (or class), or a list of objects (and or classes) one of
* which of each the direct, indirect and accessory objects of the current
* action must match in order for this Rule to match. (The accessory
* object is only relevant if the TIAAction extension is in use).
* [RULES EXTENSION]
*/
// dobj = []
// iobj = []
// aobj = []
/*
* An object, class, or other value, or a list of objects and/or classes
* or of other values, one of which must match the matchObj property of
* our rulebook (which is set by the first parameter of a call to that
* RuleBooks's follow() method) for this Rule to match. [RULES EXTENSION]
*/
// matchObj = []
/*
* An object in the presence of which the actor must be for this rule to
* match. Presence normally means in the same room, but if this property
* is defined as a list and the first item in the list is a property
* pointer (&canSee, &canHear, &canReach, &canSmell), this property will
* be used to test for tne appropriate sense connection between the actor
* and at least one of the other items in the list instead. [RULES EXTENSION]
*/
// present = []
;
#ifdef __DEBUG
/* [RULES EXTENSION] include 'rules' in the list of debugging options. */
modify DebugCtl
/* list of all debugging options */
all = inherited + 'rules'
;
#endif
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1