I didn’t have the $ trick when I wrote PQ but that hasn’t stopped me from starting to use it when I’m going back over the PQ code looking for errors and omissions ![]()
A person could probably also write a TADS function to iterate over the source files and look for duplicate tokens in vocabWords fields, eliminating one and pasting the $ before the other…
Bumping with a little observation that came out of some performance testing I’ve been doing. Figured it’s not quite worth its own thread but it’s probably worth knowing, so figured this is as close to a “general TADS3 discussion” thread as we’ve got.
Anyway: if you have a Vector instance, call it foo, and some element of it bar, then foo.removeElementAt(foo.indexOf(bar)) performs better than foo.removeElement(bar). Which is not what I would’ve expected.
It is much worse if bar is an object than, for example, an integer. But it’s still true for integers.
Here’s a little demonstration that creates a 65535-element Vector of sequential integers, creates two copies of it, shuffles the original into random order, and then traverses the shuffled list removing each element in it from one un-shuffled copy via removeElementAt() and indexOf, and then does the same only with removeElement() on the second unshuffled copy:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
#include <date.h>
#include <bignum.h>
class Foo: object
active = nil
isActive() { return(active == true); }
;
versionInfo: GameID;
gameMain: GameMainDef
count = 65535
newGame() {
local i, l0, l1, ref, ts;
// Create a vector of sequential integers.
ref = new Vector(count);
for(i = 0; i < count; i++) ref.append(i);
//for(i = 0; i < count; i++) ref.append(new Foo());
// Make two copies of it.
l0 = new Vector(ref);
l1 = new Vector(ref);
// Shuffle the original vector.
shuffle(ref);
t3RunGC();
ts = getTimestamp();
ref.forEach(function(o) { l0.removeElementAt(l0.indexOf(o)); });
aioSay('\nremoveElementAt()/indexOf() time:
<<toString(getInterval(ts))>>\n ');
t3RunGC();
ts = getTimestamp();
ref.forEach(function(o) { l1.removeElement(o); });
aioSay('\nremoveElement() time:
<<toString(getInterval(ts))>>\n ');
}
getTimestamp() { return(new Date()); }
getInterval(d) { return(((new Date() - d) * 86400).roundToDecimal(3)); }
// Fisher-Yates shuffle.
shuffle(l) {
local i, k, tmp;
for(i = l.length; i >= 1; i--) {
k = rand(l.length) + 1;
tmp = l[i];
l[i] = l[k];
l[k] = tmp;
}
}
;
On my machine that produces:
removeElementAt()/indexOf() time: 6.198
removeElement() time: 8.713
If you comment out the first for() loop (the one that populates the list with sequential integers) and uncomment the second (which fills the list with instances of the Foo class) it becomes:
removeElementAt()/indexOf() time: 8.603
removeElement() time: 18.365
Anyway, just figured this was worth knowing.
This came out of a bunch of refactoring I’ve been doing. Specifically, I’ve been re-working all the rule engine and derived modules because I discovered an assumption I’d made about performance was wrong—I’d been doing a bunch juggling to keep the number of rules evaluated each turn low, by keeping track of which rulebooks were active at any given time and adding or removing them from the rule engine’s lists when they changed state. This turns out to be substantially less performant by just leaving everything in the list(s) and evalutating each rules’ active property/method every turn.
Here’s another minor scrap of TADS3 that came out of testing.
Often I find myself wanting to know what the upper bound on the size of some T3 data structure is. For example the maximum number of elements in a LookupTable. This is a slightly subtle question, as you can make LookupTable instances with numbers of elements that will not cause problems when created but will cause exceptions when accessed in various ways, for example if you do a table.keysToList().
Anyway, here’s a little code to find an upper bound by wrapping a callback function in a try/catch block and iterating until the maximum value (that doesn’t throw an exception) is found:
function findMaxValue(v0, v1, cb, maxTries?) {
local i, j, n;
maxTries = ((maxTries != nil) ? maxTries : 65535); // max tries
n = 0; // current tries
while(1) {
if(n >= maxTries)
return(nil);
i = (v1 + v0) / 2; // current guess at value
if(i == j) // guess same twice, done
return(i);
j = i; // remember last guess
n += 1; // increment tries
try {
cb(i); // invoke callback
if(i > v0) // update lower bound
v0 = i;
}
catch(Exception e) {
v1 = i; // update upper bound
}
}
}
The args are:
v0andv1lower and upper integer values to trycbcallback function. it will be repeatedly called with integer arguments betweenv0andv1maxTriesoptional maximum number of iterations. default is 65535
Here’s an example of use. It finds the maximum number of keys in a LookupTable that won’t cause an exception when keysToList() is used:
v = findMaxValue(1, 65535, function(n) {
local i, l, table;
// Create a lookup table
table = new LookupTable();
// Add n arbitrary elements to it.
for(i = 0; i < n; i++) table[i] = i;
// Get the keys as a list. This has an upper
// bound of 13106, for some reason.
l = table.keysToList();
// Meaningless conditional so the compiler doesn't
// complain that we assigned a value to l and then
// didn't do anything with it.
if(l.length()) {}
});
In this case, v should, after a few seconds, turn out to be 13106.
it’s very curious, because 13106 multiplied by 5 gives 65530 and by 10 gives 131060, that is, the overflow exception seems to happen when reaching the next power of two (2^16 in the first case…) so I think the overflow is caused by overflowing at the the end of a 64K two-byte array.
(ugly remnant of the accursed segmented architecture of the 8086 ?)
Best regards from Italy,
dott. Piergiorgio.
Bumping this thread for another “worth posting but not worth its own thread” thing because this seems to be the closest thing to a general TADS3 discussion thread we have.
This is some code I put together for debugging backrefs/refcounts for instances of specific classes.
This is out of the procgen code I’m (still) working on. Every time a “dungeon” is generated there are a lot of temporary, just-for-the-generation-process objects created. The idea is that all of these objects get their refcount set to zero during cleanup after the generation process is complete, which means they’ll end up garbage collected. But occasionally there are situations where there’ll be a dangling reference left after cleanup, which if unfixed leads to a memory leak (as more and more stray instances stick around after each successive generation and cleanup).
Anyway, here’s the code. The bit designed to be directly interacted with is the searchObjects() global function, which takes a class as its argument. There’s also a “private” __searchObject() class that searchObjects() uses internally but isn’t of much use outside of that.
#ifdef __DEBUG
searchObjects(cls) {
local n, r;
n = 0;
forEachInstance(cls, { x: n += 1 });
if(n == 0)
"\nNo instances.\n ";
else
"\nTotal of <<toString(n)>> instances.\n ";
r = new Vector();
forEachInstance(TadsObject, function(x) {
local l;
l = __searchObject(x, cls);
if(l.length == 0)
return;
r.append([ x, l ]);
});
if(r.length() == 0) {
"\nNo matches.\n ";
} else {
local ar, i, j, txt0, txt1;
"\n<<sprintf('%_.-30s %_.-30s', 'OBJECT', 'PROPERTY')>>";
for(i = 1; i <= r.length; i++) {
ar = r[i];
for(j = 1; j <= ar[2].length; j++) {
if(j == 1) txt0 = ar[1];
else txt0 = '';
txt1 = ar[2][j];
}
"\n<<sprintf('%_.-30s %_.-30s', toString(txt0),
toString(txt1))>>";
}
}
}
__searchObject(obj, cls) {
local r, v;
r = new Vector();
obj.getPropList().forEach(function(x) {
if(!obj.propDefined(x, PropDefDirectly)) return;
if((x == &symtab_) || (x == &reverseSymtab_))
return;
switch(obj.propType(x)) {
case TypeList:
if(obj.(x).valWhich(
{ y: isType(y, cls) })
!= nil)
r.append(x);
break;
case TypeObject:
v = (obj).(x);
if(isLookupTable(v)) {
v.forEachAssoc(function(foo, bar) {
if(isType(foo, cls)
|| isType(bar, cls))
r.append(x);
});
} else if(isCollection(v)) {
if(v.valWhich({
y: isType(y, cls)
}) != nil)
r.append(x);
} else {
if(isType(v, cls))
r.append(x);
}
break;
default:
return;
}
});
return(r.toList());
}
#endif // __DEBUG
As an example of use, for debugging I’ve defined a >REFCOUNT system action, which looks like:
DefineSystemAction(Refcount)
_search = DungeonBranch
execSystemAction() {
searchObjects(_search);
}
;
VerbRule(Refcount) 'refcount' : RefcountAction
verbPhrase = 'refcount/refcounting';
This is just a wrapper for the searchObjects() function, calling it with a defined class as its argument. This could be cleaner…parsing the class from the command or something…but I’m generally only doing this for one class at a time, doing it a bunch of times while I’m doing it, and then I’m not doing it again unless I’m more careless than usual. So this approach works fine. Anyway, here’s sample output:
>refcount
Total of 3 instances.
OBJECT........................ PROPERTY......................
{obj:BranchAndBottleneck}..... _branches.....................
This tells me that I’ve got three (almost-)orphaned instances of DungeonBranch, and they’re referenced in instances of the BranchAndBottleneck class (a dungeon template), in the _branches property.
This works for instances that are referenced by properties directly, instances that occur in properties that are lists and vectors, and instances that are either a key or value in a lookup table.
Not sure how useful this is to a general audience (it’s something you only have to worry about if you’re generating stuff on the fly) but it seems worth putting out there.
Some more light thread necromancy because I think this is still the closest thing we have to a general TADS discussion thread.
Anyway, just pushed an update to the adv3Utils module I use as a junk drawer for general little TADS3/adv3 tweaks. In this case it was to add an OrdinalThing class.
OrdinalThing
Properties
-
ordinalNumber = nilThe number of this instance. The value must be an integer.
-
ordinalVocab = nilThe vocabulary to use for the ordinal vocabulary. This can be either a single single-quoted noun (
'pebble') or a list of single-quoted nouns ([ 'pebble', 'rock' ]).If
ordinalVocabisnil, the object’snounlist will be used instead. -
ordinalDisambig = trueIf boolean
truedisambiguation will automagically use ordinal numbers and the disambiguation will be sorted by each object’sordinalNumber.Default is
true.
Methods
-
addOrdinalVocab(n, str)Automatically called by
initializeThing(). Will be called withnequal to the value ofordinalNumberandstrequal to each value defined forordinalVocab.You generally won’t have to call this directly unless you’re fussing around with objects after they’re initialized.
Template
The OrdinalThing template is largely identical to the basic Thing template:
pebble01: OrdinalThing 'pebble' 'pebble number one' +1
"There's a one painted on it. "
;
The +1 gives this instance’s ordinalNumber. A single-quoted noun (or a List of them) could be added after the +1 if a different list of nouns is to be used for the ordinal vocabulary. In this case pebble, the object’s only noun, will be used instead.
Objects can also be declared without the template like a standard Thing:
pebble02: OrdinalThing 'pebble' 'pebble number two'
"There's a two painted on it. "
ordinalNumber = 2
;
Having declared the above, the following will all work:
>X FIRST PEBBLE
>X PEBBLE NUMBER ONE
>X PEBBLE #1
>X #1 PEBBLE
>X NUMBER ONE PEBBLE
Also, disambiguation will look like:
>X PEBBLE
Which pebble do you mean, the first pebble, or the second pebble?
Discussion
About half of this can be accomplished “directly” by just declaring adjectives on the objects. >X FIRST PEBBLE just requires declaring 'first' as an adjective in the object’s vocabWords. But this doesn’t work by default with a construction like >X PEBBLE NUMBER ONE, because “noun adjective adjective” isn’t a standard adv3 noun phrase format.
Under the hood this is works the same way declaring the vocabWords as ''(first) (number) (one) (#1) pebble one/pebble'. That’s declaring the noun as a non-weak adjective and the number as a weak noun. This means >X ONE will not resolve “one” as a noun phrase but, >X PEBBLE NUMBER ONE will.
The class also handles setting up the ordinal-based disambiguation, but that’s just straightforward setting of the disambigName and disambigPromptOrder. So it’s taking care of some bookkeeping but isn’t handling anything particularly confusing or fiddly. And I find the noun/adjective/weak token tweaking very fiddly, so don’t have to remember the whole ritual every time I declare an object.
Anyway, it’s another one of those “not a big deal but took me longer than I would’ve liked” kind of things, so figured I’d put it out there.
I see “about” is doing a lot of work when the system manual de#drives lists as being limited to “about 13100” elements.
Another thread bump because, again, I think it’s the closest thing to a “general TADS3 discussion” thread we have.
Added another little snippet to the adv3Utils library, this time a couple of lines to make it easier to modify an Action’s indirect object scope list. By default Action.objInScope() and Action.getScopeList() only affect the direct objects’ scope, not any indirect objects’ (which end up inheriting the default behavior on IobjResolver).
It’s a little patch that uses iobjInScope() and iobjScopeList() as objInScope() and getScopeList() (respectively) for indirect objects if they’re defined on the Action:
modify Action
iobjInScope = nil
iobjScopeList = nil
;
modify IobjResolver
objInScope(obj) {
if(action_.propType(&iobjInScope) != TypeNil)
return(action_.iobjInScope(obj));
return(inherited(obj));
}
getScopeList() {
if(action_.propType(&iobjScopeList) != TypeNil)
return(action_.iobjScopeList());
return(inherited());
}
;
This coming out of a bunch of very bewildering testing involving a lot of actions involving out-of-scope objects. Which, incidentally interact in a number of very counterintuitive ways with Unthing instances. For example an Unthing is “visible” in its location and not in any other location. Which makes sense in the context (since Unthing basically exists specifically to tweak the messages involving non-present objects in specific scopes) but can get very messy when noun resolution scope changes.
Another smattering of tiny adv3 tweaks.
This time it’s some code to handle something I’ve taken a swing at before, specifically how to handle objects with dynamic vocabulary.
The builtin ThingState almost does what I want, but a) it only allows one state/vocabulary to be active at once, and b) the only other behavior it changes is the naming of the object e.g. in list contexts.
What I need is a way to have an object reflect any of a number of possible player knowledge states, specifically where the changes aren’t necessarily triggered in a specific order and they aren’t necessarily cumulative.
So an NPC might be initially described as a guy in a Zork t-shirt. The player might at some point learn that his name is Bob, that he’s the player’s neighbor, or that Bob is a werewolf. But not necessarily in that order.
With stock ThingState the problem is if the “werewolf” state is active, then the vocabulary associated with the “neighbor” state is active unless it is also defined on the “werewolf” state. Which it can’t be if you don’t know that the player will have discovered the former fact before the latter.
Anyway, the tweak(s) I’ve added to the adv3Utils module are:
Add a generic matchStateToken() method to ThingState
This is borrowed from the adv3Patches module. It adds a couple of fixes for ThingState vocabulary matching, specifically it treats tokens as case-insensitive and it handles possessives:
modify ThingState
matchStateToken(tok) {
tok = tok.toLower();
return(stateTokens.indexWhich(function(o) {
o = o.toLower();
if(o.endsWith('\'s'))
o = o.substr(1, o.length() - 2);
return(o == tok);
}) != nil);
}
;
Update ThingState.matchName() to allow multiple states to match vocabulary
First we define an active property and an order property.
The order property is used elsewhere but I won’t get into it here, right now all that matters is that it being a number greater than -1 identifies a ThingState as using the new multi-state vocabulary matching.
The active property is a flag to enable/disable the state. This starts out nil by default and is toggled by whatever game logic reveals the knowledge associated with the state.
Then we basically just reproduce the stock ThingState.matchName() logic, with the caveats:
- If the state doesn’t have an
ordergreater than -1 then the stock behavior is used (so existingThingStateswill work as before) - If the state does have an
orderand is notactivematching immediately fails - If another state is also active and matches the token, processing continues (instead of immediately failing, as in the stock method)
The code:
modify ThingState
active = nil
order = -1
isActive() { return(isMulti() ? (active == true) : true); }
isMulti() { return(order > -1); }
matchName(obj, origTokens, toks, states) {
local i, l, len, tok;
if(!isActive())
return(nil);
len = toks.length();
for(i = 1; i <= len; i+= 2) {
tok = toks[i];
if(matchStateToken(tok))
continue;
l = states.subset({
x: x.matchStateToken(tok) != nil
});
if(l.indexWhich({ x: x.isMulti() && x.isActive() })
!= nil)
continue;
if(l.length > 0)
return(nil);
}
return(obj);
}
;
Add a stateDesc property to ThingState
This just adds a state-specific description to the object that will be displayed if the given state is the current state.
Note that this has nothing to do with the multi-state logic above; this only applies to the state that’s returned by Thing.getState():
modify ThingState
stateDesc = ""
;
modify Thing
examineStatus() {
local st;
inherited();
if((st = getState()) != nil)
st.stateDesc;
}
;
Implement something similar for rooms
To get multi-state rooms that work as above, we now add the room-specific description properties to ThingState and tweak Thing.lookAroundWithinDesc() to use them:
modify ThingState
roomDesc = ""
roomFirstDesc { roomDesc; }
roomDarkDesc = ""
roomRemoteDesc(actor) {}
;
modify Thing
lookAroundWithinDesc(actor, illum) {
local pov, st;
inherited(actor, illum);
if((st = getState()) == nil)
return;
if(illum > 1) {
pov = getPOVDefault(actor);
if(!actor.isIn(self) || (actor != pov)) {
st.roomRemoteDesc(actor);
} else if(actor.hasSeen(self)) {
st.roomDesc;
} else {
st.roomFirstDesc;
}
} else {
st.roomDarkDesc;
}
}
;
Some code for declaring arbitrary message parameter substitutions
Finally we declare a MessageToken class and some init gymnastics to declare arbitrary message parameter strings. This is almost what you get out of Thing.globalParamName, but this allows greater flexibility (multiple substitutions on a single object, or multiple objects using the same substitution) :
class MessageToken: object
id = nil
prop = nil
token = nil
obj = nil
;
messageTokensPreinit: PreinitObject
execAfterMe = [ MessageBuilder ]
_messageTokens = perInstance(new Vector)
execute() {
initMessageTokens();
}
initMessageTokens() {
forEachInstance(MessageToken, { x: addMessageToken(x) });
}
addMessageToken(obj) {
if(!isMessageToken(obj)) return(nil);
_messageTokens.appendUnique(obj);
#ifdef __DEBUG
if(obj.id != obj.id.toLower()) {
aioSay('\n===WARNING===\n ');
aioSay('\nconverting MessageToken id <<obj.id>>
to lower case\n ');
aioSay('\n===WARNING===\n ');
}
#endif // __DEBUG
obj.id = obj.id.toLower();
obj.token = 'token_' + obj.id;
langMessageBuilder.paramList_
= langMessageBuilder.paramList_.append(
[ obj.id, obj.prop, obj.token, nil, nil ]);
return(true);
}
;
messageTokensInit: InitObject
execute() {
initMessageTokens();
}
initMessageTokens() {
messageTokensPreinit._messageTokens.forEach({
x: addMessageToken(x)
});
}
addMessageToken(obj) {
if(!isMessageToken(obj)) return(nil);
langMessageBuilder.nameTable_[obj.token] = obj.obj;
return(true);
}
;
Putting it all together
A simple “game” that illustrates what this gets us. We define a couple of states on the starting room, with the states controlled by a <.reveal> tag in the sign description:
#include <adv3.h>
#include <en_us.h>
#include "adv3Utils.h"
versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;
startRoom: Room '{void}'
""
vocabWords = '(featureless) (some) (odd) void/room'
allStates = [ nonVoid, void ]
getState() {
return(gRevealed('void') ? void : nonVoid);
}
voidName() { return(getState().voidName); }
;
+nonVoid: ThingState
order = 1
active = true
stateTokens = [ 'odd' ]
roomDesc = "This is an odd room with a sign on the wall. "
voidName = 'Some Room'
;
+void: ThingState
order = 2
active = (gRevealed('void'))
stateTokens = [ 'featureless', 'void' ]
roomDesc = "This is a featureless void with a sign on the wall that
totally doesn't count as a feature. "
voidName = 'Void'
;
+sign: Decoration 'sign' 'sign'
"<q>This is the Void.</q> <.reveal void> ";
+me: Person;
MessageToken 'void' ->(&voidName) @startRoom;
Thrilling transcript:
Some Room
This is an odd room with a sign on the wall.
>x odd room
This is an odd room with a sign on the wall.
>x void
You see no void here.
>x sign
"This is the Void."
>x void
This is a featureless void with a sign on the wall that totally doesn't count
as a feature.
>x odd room
This is a featureless void with a sign on the wall that totally doesn't count
as a feature.
This is pretty great. Three years in, I have kludged my way around this capability in the most ugly ways imaginable. sigh Do I REALLY refactor at this point???
I also appreciate your discipline in “Patch” definition. My own adv3_jjmcc is, at this point, a grotesque, inseparable melange of bugfixes and extensions.
That’s the third? Fourth? Approach to the problem I’ve “formally” done. As in code in a repo instead of code doodles in a scratch directory somewhere.
It’s one of those problems where you get stuck in loops going “Option A is bad because of [reasons], so I should do B, which fixes the problems of A”, then “Option B is bad because of [different reasons], so I should do C, which fixes the problems of B”, and then “Option B is bad because of [third set of reasons], so I should do A, which fixes the problems of C”.
Another fairly minor adv3 tweak, this one involving doors.
I’ve always low-key disliked how adv3 handles declaring doors by default. It bugs me that you have to manually declare a bunch of stuff that should be obvious from context. And if your doors share vocabulary and/or descriptions on both sides (which most of my doors do) then you end up having to manually do duplicate some effort (and make sure you’re keeping them in sync across revisions).
None of that’s a super big deal. None of it is exactly difficult or anything. It’s just fiddly in ways that it doesn’t have to be for most of my uses.
Anyway, here’s some code intended to make it less fiddy.
Example
Before getting to the code, here’s an example of usage. The gimmick is that now you declare a single EasyDoor instance and use it in the direction properties of both rooms it connects:
demoDoor0: EasyDoor '(wooden) door' 'door' "A wooden door. ";
southRoom: Room 'South Room'
"This is the south room. To the north is the door to the north
room. "
north = demoDoor0
;
northRoom: Room 'North Room'
"This is a the north room. The south room is south. "
south = demoDoor0
;
This creates two “real” Door instances with the vocabulary, name, and description declared on the EasyDoor instance. The doors are placed in the right rooms and connected.
The Code
First a helper class that’s just going to hold some temporary data for us during configuration:
// Helper class.
class _EasyDoorCfg: object
room = nil // Room the door is in
dir = nil // Direction of the door
construct(v0, v1) {
room = v0;
dir = v1;
}
;
Then the main class, EasyDoor:
class EasyDoor: PreinitObject
doorClass = Door // Class of door we create
mainDoor = nil // room the "masterObject" is in
name = ''
vocabWords = ''
desc = nil
_locations = nil // Internal use only
// Preinit method.
execute() {
// List for all the locations we're mentioned in.
_locations = new Vector();
// Iterate over all rooms.
forEachInstance(Room, { x: checkRoom(x) });
// If we're not in exactly two locations something's wrong,
// give up.
if(_locations.length != 2)
return;
// Set up the "real" doors.
initDoor();
// Done with the locations list.
_locations = nil;
}
// See if we're in the given room.
checkRoom(rm) {
// Check all direction properties on the room to see if
// any of them point to us.
Direction.allDirections.forEach(function(d) {
local dst;
if((dst = rm.(d.dirProp)) == nil)
return;
// Remember that this
if(dst == self) {
_locations.append(new _EasyDoorCfg(rm, d));
}
});
}
// Create the "real" doors we represent.
initDoor() {
local d0, d1, dst, src;
// We need to instances of our door class.
d0 = doorClass.createInstance();
d1 = doorClass.createInstance();
/*
if(keyList) {
d0.keyList = keyList;
d1.keyList = keyList;
}
*/
// Convenience variables for our saved locations.
src = _locations[1];
dst = _locations[2];
// Make sure one of the doors is the "main" one. This
// is used to figure out which door is what adv3 calls
// by the unfortunate name "masterObject".
if(mainDoor == nil)
mainDoor = src.room;
// Basic setup. This is where we move the doors into
// their rooms, hook them up to the direction properties,
// and set their destination/otherSide.
_initDoor(d0, src, d1, dst);
_initDoor(d1, dst, d0, src);
_tweakVocab(d0);
_tweakVocab(d1);
// Final configuration. This is so individual door classes
// can implement their own setup methods. We do this
// after both doors have done _initDoor() above so each
// instance knows about its other half.
d0.configureDoor(self);
d1.configureDoor(self);
}
// Basic setup. door and otherDoor are instances of doorClass,
// and cfg and otherCfg are instances of _EasyDoorCfg.
_initDoor(door, cfg, otherDoor, otherCfg) {
// Put the door in its room.
door.moveInto(cfg.room);
// Connect the door to the room's direction property.
cfg.room.(cfg.dir.dirProp) = door;
// Declare the "masterObject".
if(cfg.room == mainDoor)
door.masterObject = door;
else
door.masterObject = otherDoor;
// Set the other side.
door.otherSide = otherDoor;
}
// Set the name, vocabulary, and description on a door instance
// if a) the door class doesn't come with one or b) we have one
// set.
_tweakVocab(d) {
if((d.name == '') || (name != ''))
d.name = name;
if((d.vocabWords == '') || (vocabWords != ''))
d.initializeVocabWith(vocabWords);
if((d.propType(&desc) != TypeNil)
|| (propType(&desc) != TypeNil))
d.setMethod(&desc, {: "<<desc>>" });
}
;
Then we declare in a header somewhere a template for the new class. This is just so the EasyDoor has a declaration syntax like a generic Thing (even though it isn’t a Thing at all).
EasyDoor template ->mainDoor? 'vocabWords'? 'name'? "desc"?;
Finally a little tweak to ThroughPassage to add a stub method:
// but we're technically happy handling any kind of travel connector.
// But "door" is a lot easier to type.
modify ThroughPassage
configureDoor(obj) {}
;
Usage
The properties worth calling out on EasyDoor are:
-
desc = nilA double-quoted string or method that displays the doors’ description. If none is specified, the created “real” doors will inherit their description from the door class.
-
doorClass = DoorThe class to use for the “real” door instances. Should work with any kind of
TravelConnectorbut we use “door” in methods and properties because it’s easier to type. -
mainDoor = nilThe “main” door. This is used to set what adv3 refers to by the unfortunate name “masterObject”. This is mostly arbitrary: on simple doors it just selects the instance that’s used to figure out whether or not the door is open.
-
name = ''Name for the doors. If none is specified the door instances will inherit from the door class.
-
vocabWords = ''Vocabulary for the doors. If none is specified the door instances will inherit from the door class.
In addition to the above, we tweak ThroughPassage to include a new method:
-
configureDoor(obj)This will be called during preinit on each door instance created by
EasyDoor, with theEasyDoorinstance as its argument.The call will be made after both sides of the door have been created, placed, and connected. So each instance can rely on its
location,otherSideand so on to be correct.This is for per-subclass setup; I have one-way doors and shortcuts (door that at first you can only open from one side but after being opened the first time can be opened from either side) and so on that all require additional tweaking.
Like most of these little tweaks this is in the adv3Utils module now.
Door definition has always felt clumsier than it needed to be, for all the reasons you cited. I think what I like the most about this is the charming aliasing of “object instancing” to PreInitObject
THAT’S JUST SO COOL.
Too late to help me now, but likely to be added to adv3_jjmcc going forward.
More stupid door tricks. This time a deadbolt class, for things that can be unlocked without a key, but only from one side.
The Deadbolt class
We make the new class a subclass of Lockable. Lockable objects can, by default be freely locked and unlocked (without need for a key, for example). We’re basically just creating a version where that only works from one side, and adding a bunch of special messages tor the new action verification failures that can arise as a result.
We figure out which side we’re on by checking if the instance receiving the action is the masterObject. So this really only makes sense for things like doors that have multiple sides.
// Class of lockable objects that can only be unlocked from one side.
// Really only makes sense for things like doors that have a "masterObject",
// which will be the side the lock can be locked and unlocked from.
class Deadbolt: Lockable
// By default we allow ourselves to be unlocked via implicit
// action (when unlocking is otherwise possible). If nil
// then an explicit >UNLOCK action will be required.
allowImplicitUnlock = true
cannotUnlockMsg = ((masterObject == self)
? &cannotUnlockDeadbolt : &cannotUnlockDeadboltHere)
// Returns boolean true if this is the side the deadbolt is on.
isDeadboltReachable() {
return(masterObject == self);
}
dobjFor(Lock) {
verify() {
// Custom failure when trying to lock the door
// from the non-deadbolt side.
if(!isLocked() && !isDeadboltReachable())
illogical(&cannotLockDeadboltHere);
inherited();
}
}
dobjFor(Unlock) {
verify() {
if(isLocked()) {
// If we're locked and we're the deadbolt
// side we skip verification, allowing the
// action to succeed.
if(isDeadboltReachable())
return;
// If we're locked and not on the deadbolt
// side we use a deadbolt-specific failure
// message.
illogical(&cannotUnlockDeadboltHere);
return;
}
inherited();
}
}
dobjFor(Open) {
verify() {
// Special case when we could theoretically
// implicitly unlock the door but allowImplicitUnlock
// is not true.
if(isLocked() && isDeadboltReachable
&& gAction.isImplicit && !allowImplicitUnlock)
illogicalNow(&cantImplicitUnlockDeadbolt);
inherited();
}
}
;
Then we declare the new library messages:
modify playerActionMessages
// Verify failure when Deadbolt.allowImplicit is nil and the
// action is implicit.
cantImplicitUnlockDeadbolt = '{The dobj/he} {is dobj} locked but
it looks like {you/he} can unlock {it dobj/him} from this
side. '
// Verify failure for >LOCK when on the other side of the door.
cannotLockDeadboltHere = '{The dobj/he} {do dobj}n\'t appear
to be lockable from this side. '
// Verify failure for >UNLOCK when on the other side of the door.
cannotUnlockDeadboltHere = '{The dobj/he} {do dobj}n\'t appear
to open from this side. '
// Obscure; this is for when cannotUnlockMsg is needed and it IS NOT
// because of being on the wrong side of the door. Not needed
// for stock Deadbolt doors, might be used in things like
// IndirectLockable deadbolt doors.
cannotUnlockDeadbolt = '{The dobj/he} open{s dobj} from this side,
but {you/he} {has} to do it {yourself}. '
// Not used. Theoretical alternate success message that could
// be used in dobjFor(Unlock) { action() {} } if desired.
okayUnlockDeadbolt = '{You/He} unlock{s} {the dobj/him} from
this side. '
;
For convenience we can also declare a DeadboltDoor class that’s, obviously, just a Deadbolt that’s also a Door:
class DeadboltDoor: Deadbolt, Door;
The main Deadbolt class isn’t declared this way to allow it to be used as a mixin on other kinds of travel connector.
Properties
There’s one property of note on Deadbolt:
-
allowImplicitUnlock = trueBoolean flag. If
true, the deadbolt can be unlocked by an implicit action. Ifnilunlocking requires an explicit>UNLOCKcommand.Default is
true.
An example
Here’s a simple demo world consisting of two rooms connected by a deadbolt door. The start room contains the side that can’t be unlocked, so there’s also a side passage to get to the other side to unlock it.
#include <adv3.h>
#include <en_us.h>
#include "adv3Utils.h"
versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;
class DemoDoor: Deadbolt, Door '(deadbolt) (wooden) door' 'door'
"A wooden door with a deadbolt. The deadbolt is on
<<((masterObject == self) ? 'this' : 'the other')>>
side. "
;
southRoom: Room 'South Room'
"This is the south room. To the north is the door to the north
room and to the east is the alternate route. "
north = demoDoor0
east = alternateRoom
;
+me: Person;
+demoDoor0: DemoDoor
destination = northRoom
masterObject = demoDoor1
;
alternateRoom: Room 'Alternate Route'
"This is the alternate route. The north room is to the northwest
and the south room is to the west. "
west = southRoom
northwest = northRoom
;
northRoom: Room 'North Room'
"This is a the north room. The south room is south and the alternate
route is to the southeast. "
south = demoDoor1
southeast = alternateRoom
;
+demoDoor1: DemoDoor destination = southRoom;
This uses the regular adv3 door declaration syntax. To illustrate using the EasyDoor class from last time with a custom door class, that looks like:
#include <adv3.h>
#include <en_us.h>
#include "adv3Utils.h"
versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;
demoDoor: EasyDoor ->northRoom '(deadbolt) (wooden) door' 'door'
"A wooden door with a deadbolt. The deadbolt is on
<<((masterObject == self) ? 'this' : 'the other')>>
side. "
doorClass = DeadboltDoor
;
southRoom: Room 'South Room'
"This is the south room. To the north is the door to the north
room and to the east is the alternate route. "
north = demoDoor
east = alternateRoom
;
+me: Person;
alternateRoom: Room 'Alternate Route'
"This is the alternate route. The north room is to the northwest
and the south room is to the west. "
west = southRoom
northwest = northRoom
;
northRoom: Room 'North Room'
"This is a the north room. The south room is south and the alternate
route is to the southeast. "
south = demoDoor
southeast = alternateRoom
;
In action
A thrilling transcript:
South Room
This is the south room. To the north is the door to the north room and to the
east is the alternate route.
>n
(first trying to unlock the door)
The door doesn't appear to open from this side.
>e
Alternate Route
This is the alternate route. The north room is to the northwest and the south
room is to the west.
>nw
North Room
This is a the north room. The south room is south and the alternate route is
to the southeast.
>s
(first unlocking the door, then opening it)
South Room
This is the south room. To the north is the door to the north room and to the
east is the alternate route.
Still jiggling doorknobs. In this case I revisited the EasyDoor class to extend it. This is all in the adv3Utils repo, but the summary is:
- Renamed
EasyDoortoDoorPair. In the theory that it’s slightly more descriptive. - The value of
doorClasscan now be aList, to allow different classes to be used for either side of the door. - You can now use the
+syntax to add a specific instance to be used instead of a created instance ofdoorClass. If this is done the properties on theDoorPairinstance (name,vocabWords, anddesc) will only be set if the instance’s corresponding prop is unset/empty.
Some Examples
Using a single doorClass:
// Set the door class
myDoor: DoorPair '(auto) (closing) door' 'door'
"An auto-closing door. "
doorClass = AutoClosingDoor
;
Using a different class for each side of the door:
class WoodenDoor: Door '(wooden) door' 'door' "A wooden door. ";
class MetalDoor: Door '(metal) door' 'door' "A metal door. ";
// The "main" door (in northRoom) will use MetalDoor, the other side will
// use WoodenDoor.
myDoor: DoorPair ->northRoom
doorClass = static [ MetalDoor, WoodenDoor ]
;
Declaring an instance. The two examples below are basically the same as the one above, only with a slightly different description on the metal door:
// The "main" door will be the declared instance,
// and the other side will be default WoodenDoor instance.
// Basically the same as the example above, only the metal
// door gets a different description.
myDoor: DoorPair ->northRoom
doorClass = WoodenDoor
;
+MetalDoor desc = "This is a slightly different metal door. ";
// Both doors are statically declared.
myDoor: DoorPair ->northRoom;
+MetalDoor desc = "This is a slightly different metal door. ";
+WoodenDoor;
The Updated Source
This is mostly the same as presented in the post about EasyDoor above, but with the name changed and the additions noted above.
// Helper class.
class _DoorPairCfg: object
room = nil // Room the door is in
dir = nil // Direction of the door
construct(v0, v1) {
room = v0;
dir = v1;
}
;
// Preinit functions moved to a standalone object. Because it turns out
// we want to iterate over all travel connectors and all DoorPair instances
// and we can't just do one in the other (we don't want to have to iterate
// over all TravelConnectors for each DoorPair).
doorPairPreinit: PreinitObject
execute() {
forEachInstance(TravelConnector, { x: x.initializeDoorPair() });
forEachInstance(DoorPair, { x: x.initializeDoorPair() });
}
;
// Modify TravelConnector check the instance and if it's the child of
// an DoorPair, ping it to let it know.
modify TravelConnector
// Flag used to distinguish between connectors that are created
// by DoorPair and ones that are declared statically.
doorPairFlag = nil
initializeDoorPair() {
if(isDoorPair(location))
location.addDoor(self);
}
;
class DoorPair: object
doorClass = Door // Class of door we create
mainDoorLocaiton = nil // room the "masterObject" is in
name = ''
vocabWords = ''
desc = nil
_locations = nil // Internal use only
_definedDoors = nil
// Called by any travel connector that is our lexical child,
// during preinit. We add it to a list so we know to create
// fewer door instances later.
addDoor(obj) {
if(!isTravelConnector(obj))
return(nil);
if(_definedDoors == nil)
_definedDoors = new Vector(2);
_definedDoors.append(obj);
return(true);
}
initializeDoorPair() {
// List for all the locations we're mentioned in.
_locations = new Vector();
// Iterate over all rooms.
forEachInstance(Room, { x: checkRoom(x) });
// If we're not in exactly two locations something's wrong,
// give up.
if(_locations.length != 2)
return;
// Set up the "real" doors.
createDoors();
// Done with the locations list.
_locations = nil;
}
// See if we're in the given room.
checkRoom(rm) {
// Check all direction properties on the room to see if
// any of them point to us.
Direction.allDirections.forEach(function(d) {
local dst;
if((dst = rm.(d.dirProp)) == nil)
return;
// Remember that this
if(dst == self) {
_locations.append(new _DoorPairCfg(rm, d));
}
});
}
// Logic that actually creates any door instances we need, without
// doing any configuration.
_createDoors() {
// A temporary-ish vector to hold the door instances. In
// theory we could already have some because we now support
// declaring them in source but if the prop is nil we're
// starting from scratch.
if(_definedDoors == nil)
_definedDoors = new Vector(2);
// We need two instances of our door class. We might
// have some already declared, so just add more until
// we have enough.
while(_definedDoors.length < 2) {
_definedDoors.append(_getDoorClass().createInstance());
// Set the flag that indicates the door was created
// rather than declared. This will prevent the
// DoorPair overwriting the name and so on even
// if they're declared on the DoorPair.
_definedDoors[_definedDoors.length].doorPairFlag = true;
}
}
// Get the class for a newly-created door.
_getDoorClass() {
// Easiest case. If we're not an array type, just return
// the declared class.
if(!isCollection(doorClass))
return(doorClass);
// If we're here, we're in the middle of adding instances
// to _definedDoors. So if _defined doors is currently
// n elements long, we want the class for the n + 1th
// door. If the list is that long, just return that class.
if(doorClass.length >= _definedDoors.length + 1)
return(doorClass[_definedDoors.length + 1]);
// Nope, we can't map classes 1-to-1 with doors, so just
// use the first element of the list.
return(doorClass[1]);
}
// Create the "real" doors we represent.
createDoors() {
local d0, d1, dst, src;
_createDoors();
d0 = _definedDoors[1];
d1 = _definedDoors[2];
// Convenience variables for our saved locations.
src = _locations[1];
dst = _locations[2];
// Make sure one of the doors is the "main" one. This
// is used to figure out which door is what adv3 calls
// by the unfortunate name "masterObject".
if(mainDoorLocation == nil)
mainDoorLocation = src.room;
// Basic setup. This is where we move the doors into
// their rooms, hook them up to the direction properties,
// and set their destination/otherSide.
initDoor(d0, src, d1, dst);
initDoor(d1, dst, d0, src);
_tweakVocab(d0);
_tweakVocab(d1);
// Final configuration. This is so individual door classes
// can implement their own setup methods. We do this
// after both doors have done initDoor() above so each
// instance knows about its other half.
d0.configureDoor(self);
d1.configureDoor(self);
}
// Basic setup. door and otherDoor are instances of doorClass,
// and cfg and otherCfg are instances of _DoorPairCfg.
initDoor(door, cfg, otherDoor, otherCfg) {
// Put the door in its room.
door.moveInto(cfg.room);
// Connect the door to the room's direction property.
cfg.room.(cfg.dir.dirProp) = door;
// Declare the "masterObject".
if(cfg.room == mainDoorLocation)
door.masterObject = door;
else
door.masterObject = otherDoor;
// Set the other side.
door.otherSide = otherDoor;
}
_tweakVocab(d) {
if(d.doorPairFlag == true)
_tweakVocabDefault(d);
else
_tweakVocabSafe(d);
}
// Set the name, vocabulary, and description on a door instance
// if a) the door class doesn't come with one or b) we have one
// set.
_tweakVocabDefault(d) {
if((d.name == '') || (name != ''))
d.name = name;
if((d.vocabWords == '') || (vocabWords != ''))
d.initializeVocabWith(vocabWords);
if((d.propType(&desc) == TypeNil)
|| (propType(&desc) != TypeNil))
d.setMethod(&desc, getMethod(&desc));
}
// Set the name and so on only if the door instance doesn't
// already have one. This is so statically-declared doors
// lexically added to us can have different properties than
// the class. Really an implementation wart: this only saves
// effort if one of the instances wants to override the
// DoorPair defaults for only *some* of these properties. Because
// if it replaces all of them, then you have a complete set
// of declarations on the "custom" instance, and then another
// complete set on the DoorPair instance. Which would be the
// same amount of effort as putting everything on the second,
// "non-custom" instance instead of the DoorPair. But eh.
_tweakVocabSafe(d) {
if((d.name == '') && (name != ''))
d.name = name;
if((d.vocabWords == '') && (vocabWords != ''))
d.initializeVocabWith(vocabWords);
if((d.propType(&desc) == TypeNil)
&& (propType(&desc) != TypeNil))
d.setMethod(&desc, getMethod(&desc));
}
;
// Put a stub config method on TravelConnector; we call it "configureDoor()"
// but we're technically happy handling any kind of travel connector.
// But "door" is a lot easier to type.
modify TravelConnector
configureDoor(obj) {}
;
// Tweak configureDoor() on LockableWithKey to add any keylist on the
// DoorPair.
modify LockableWithKey
configureDoor(obj) {
if(obj.keyList)
keyList = obj.keyList;
inherited(obj);
}
;
…and the new template:
DoorPair template ->mainDoorLocation? 'vocabWords'? 'name'? "desc"?;
Closing Note
Updated because I was hammering away at the WIP and decided I really preferred to declare doors this way. And so I’d rather spend the time extending the class over manually configuring a bunch of door instances where the two sides need to be different.
Two totally not nitpicky questions:
- Would not
SimpleDoorbe a classname consistent with other adv3 streamlined implementations? (at least prior to the location based evolution) - I’m a little distracted, but how is the equal sign parsed here?
I would have expected
myDoor: DoorPair ->northRoom;
+MetalDoor desc = "This is a slightly different metal door. ";
+WoodenDoor;
Fun fact, this is how I made Doors in I Am Prey. A lot of the doors were complex objects, often with attached RFID scanner objects, doorknob objects, and cat door objects on both sides.
So doors (as well as many furnishings in the game) were outlined in the code, and those outlines are replaced with procedurally-assembled groups of objects during pre-init.
This was particularly done because players had an interest in investigating individual components of furnishings (locks, latches, knobs, grates, etc), but an industrial setting will have identical copies of these all over the places, since they’re bulk-ordered.
So pre-init assembly seemed like the way to go.
EDIT: Honestly, my preference for industrial settings has regularly caused me a lot of grief in parsers, because parsers don’t really have a good way of handling environments with many identical copies.
More personable environments with many variants of objects, collected from varied sources, are a lot easier. If your house has two chairs in a room, but they’re from two different suppliers because you didn’t buy a second one until it was needed, then they look unique and are easily put into a parser game.
I spent many days trying to figure out how to gracefully handle the situation where a player walks into a room with 20 identical desks. None of these desks have anything different, so it doesn’t matter which one you interact with, but also it would be really strange to have a classroom with one desk, y’know?
Like, this problem alone is a massive motivator for why I left Inform 7 for TADS. I just couldn’t figure out how to properly model industrial environments in I7 at all.
Yeah, in that example I was using a coding technique called “a typo”. Good catch.
As for the calling things “SimpleFoo” and so on, I’ve been moving away from that nomenclature. Mostly because I think “Simple” is too much typing for common class names. But also because a lot of the “simple” classes suffered scope creep. The “SimpleGraph” class becomes just “Graph” in the dataTypes library because it doesn’t really feel like a lightweight/minimal-feature implementation anymore.
Yeah.
It isn’t an industrial setting (my setting is an aging tourist trap whose heyday was half a century ago) but I have a couple of finding-a-needle-in-a-needle-stack kinds of problems. Like a hotel and various outdoor environments. And it’s challenging to implement a situation like the player having to search an outdoor location to find a single tree that has something carved on it. Every clearing in the woods needs to have trees, and all the trees need to have bits and pieces that can be examined and otherwise fiddled with: leaves, branches, bark, and so on. Because although the puzzle is not “just have the player examine everything” it needs to feel like an actual forest that’s full of stuff that the player can interact with, rather than a sequence of connected boxes with a couple "That’s not important"s sprinkled in it.
I also have a bunch of resource management and lightweight “base building” stuff (as in not literal base building, but in the sense that the player can gather and use resources to modify parts of the gameplay loop in various ways) so most of the “finding a needle in a needle stack” locations serve double duty as resource sources. So the player isn’t just rewarded for poking around a forest of tree variations by eventually solving a single puzzle, the environments continue to provide rewards for exploration.
Fair enough, though I will confess I view SimpleXXX slightly differently (maybe? or maybe my thresshold is just more calcified). I don’t think of SimpleXXX as necessarily a lightweight or miminal feature, only that it is much more concise to USE. By the author, I mean, in their codebase. That test still seems to apply to the DoorPair!
I started out (way back when) with a bunch of Complex[Something] classes, following the naming convention of ComplexContainer and ComplexComponent. The patient zero for that was my attempt to create furniture classes where you could just declare something like myDresser: Dresser and if the class was supposed to have a bunch of components (like a surface on top and a drawer in this case) they’d just take care of themselves. That was ComplexThing, where a Dresser ends up being a ComplexContainer, ComplexThing, Furniture.
Classes were therefore Simple because they weren’t doing anything fancy like that. So SimpleGraph was “simple” because it didn’t even attempt to provide all the methods and classes you’d expect from a general “graph as in graph theory” implementation, just the bare minimum to do things like graph traversals.
So at least in my original nomenclature “complex” things were the ones where being easier to declare than the alternative was the selling point. And, in a somewhat embarrassing coincidence, because I added a bunch of macros and other semantic sugar to the Graph class (in dataTypes) that weren’t in the original SimpleGraph, a Graph is easier to declare than a SimpleGraph.
Naming things is hard.