Problems creating a class based on ComplexContainer

I’m new to TADS 3 and trying to create a Desk class which you can put things both on and under. ComplexContainer plus a Surface and an Underside works great, as long as I copy/paste the nested object properties onto every static object. But I’m trying to generalize this to a Desk class, to reduce duplication, and running into problems.

Basically, it’s clear that my Surface and Underside exist only on class Desk itself and are shared by all objects for which Desk is the prototype. What’s the best way to define a Desk class such that all the parts of the ComplexContainer are created fresh for every static object that inherits from it?

Here’s a minimal example, a desk with a rock on it:

gameMain: GameMainDef
    initialPlayerChar = me
;

room: Room 'room' 'room'
    "You're in a minimal example with a desk. "
;
+ me: Actor;

class Desk: ComplexContainer, Heavy
    // These nested objects appear to exist only on the "class".
    subSurface: ComplexComponent, Surface { }
    subUnderside: ComplexComponent, Underside { }
;

desk: Desk {
    vocabWords = 'desk'
    name = 'desk'
    desc = "It's a desk that doesn't seem to work right. "
    // All is well when I add the nested objects on the instance like so:
    // subSurface: ComplexComponent, Surface { }
    // subUnderside: ComplexComponent, Underside { }
    location = room
}

rock: Thing {
    vocabWords = 'rock'
    name = 'rock'
    desc = "It's a rock that's not so hot either. "
    location = desk.subSurface
}

I have a couple specific problems. First one is that this prints:

Room
You’re in a minimal example with a desk.
On the is a rock. On the is a rock.

Not sure what’s going on there, but I assume it shakes out of some of my other bad assumptions. The blank name seems to be because it’s using the name on class Desk, not my instance. If I give class Desk an initial value for name, then it prints that, and not the instance’s name.

Secondly, if I create more instances of Desk as static objects, it’s clear the nested Surface and Underside objects are being shared by all the objects (the rock is “already on” each of the Desks when trying to move it around).

I thought I could solve this with a constructor to instantiate the Surface and Underside, but apparently constructors aren’t called in static object instantiations, only in dynamic instantiation with new. What’s the usual way to do what I’m after here? And getting craftier, what’s a good way with debugging tools or logging statements to inspect the state of the game world so I can check my hypothesis?

Yeah, these inner objects are going to be static to the class. There’s probably a way to do what you want with something like:

class DeskSubUnderside: ComplexComponent, Underside;

class Desk: ComplexContainer, Heavy
    initializeThing()
    {
        inherited();
        subSurface = DeskSubSurface.createInstance();
        subSurface.location = self;
    }
;

But it’s kind of a nightmare to get it right. I recommend just declaring these inner objects in each object instead.

You can see an example of a class with nested objects at my post here: (TADS3) I don't understand why inner classes are doing this.

That has a lot of added complexity by allowing different properties to be set (I preferred that over requiring each instance to override the preinitThing method). I’m happy to answer any questions you have!

For anyone still wondering how to do this:

class ComplexSurface : ComplexComponent, Surface
;
class ComplexUnderside : ComplexComponent, Underside
;
class ComplexRearContainer : ComplexComponent, RearContainer
;
class ComplexRearSurface : ComplexComponent, RearSurface
;
class ComplexInnerContainer : ComplexComponent, Container
;


class Table : ComplexContainer, Heavy
    subSurface = perInstance(new ComplexSurface())
    subUnderside = perInstance(new ComplexUnderside())
    initializeThing() {
        subSurface.moveInto(self);
        subUnderside.moveInto(self);
        subSurface.targetObj = self;
        subUnderside.targetObj = self;
        inherited();
    }
    contentsListed = nil
;

Hopefully this helps.

Just ran into this weird artifact during a refactor, acharacteristically consulted the board after only two days of debug. Thanks mists of time!

After running into this a couple years ago myself I put together a furniture module for TADS3/adv3 that implements a number of (correctly) instanceable furniture types. There’s a Dresser class that gives each instance a (unique to it) drawer and top surface, for example.

Yeah, I’m finding there is a lot of finickiness with ComplexContainers. Will summarize once done, but here is a big one (to me). If an object is put into a subContainer (or any sub, really), obj.isIn(ComplexContainer) returns false! This seems at best counter intuitive, and worse obviously wrong. The fix requires a few lines inserted into the standard Thing isIn() method:

    // isIn does not detect complexContainers, counter intuitive.  change that!
    //
    isIn(obj)
    {
        local loc = location;
        
        // if we have no location, we're not inside any object 
        if (loc == nil)
        {
            /*
             *   We have no container, so there are two possibilities:
             *   either we're not part of the game world at all, or we're
             *   a top-level object, which is an object that's explicitly
             *   part of the game world and at the top of the containment
             *   tree.  Our 'isTopLevel' property determines which it is.
             *   
             *   If they're asking us if we're inside 'nil', then what
             *   they want to know is if we're outside the game world.  If
             *   we're not a top-level object, we are outside the game
             *   world, so isIn(nil) is true; if we are a top-level
             *   object, then we're explicitly part of the game world, so
             *   isIn(nil) is false.
             *   
             *   If they're asking us if we're inside any non-nil object,
             *   then they simply want to know if we're inside that
             *   object.  So, if 'obj' is not nil, we must return nil:
             *   we're not in any object, so we can't be in 'obj'.  
             */
            if (obj == nil)
            {
                /* 
                 *   they want to know if we're outside the simulation
                 *   entirely: return true if we're NOT a top-level
                 *   object, nil otherwise 
                 */
                return !isTopLevel;
            }
            else
            {
                /*
                 *   they want to know if we're inside some specific
                 *   object; we can't be, because we're not in any object 
                 */
                return nil;
            }
        }
        /* if obj is my immediate container, I'm obviously in it */
        if (loc == obj)
            return true;

        //  JJMcC changes:  if loc is a complex component, check
        //  targetObj (the component's parent)
        //  return true if in that, otherwise reassign loc to
        //  continue upward recursion
        //
        if (loc.ofKind(ComplexComponent)) {
            if (loc.targetObj == obj) return true;
            else loc = loc.targetObj;
        }
        // end of JJMcC changes

        /* I'm in obj if my container is in obj */
        return loc.isIn(obj);
    }
;

Was thinking I would provide a holistic summary, but after a major refactor instantiating class-based ComplexContainers, I’m still not confident I’ve completely explored the space. In my case, the ComplexContainers in question are MOSTLY subSurface/subContainer, but almost all also have custom tweaks. Like maybe one is not a container, another one has an additional underside, etc). Adding subLocations is not so bad, those can be done in the instance. Also, some subLocations might be Restricted, others not. Some subLocations might have custom responses to verbs (which standard dobjFor() etc macros barf on).

It is a fair question, ‘Why not just cold instantiate every single one?’ There was enough common code, and high enough instance count, that made that unattractive is the answer. So yeah, personal taste.

So, my ‘enhancements’ really fell into three (asymmetrically sized) buckets:

  • Continue to refine global behavior of ComplexContainers
//  ComplexComponents use targetObj instead of location
//  Given all the hairiness around them, was not comfortable
//  mucking with ComplexComponent.location
//  instead, adding a targetObj to generic Thing allowed ease of
//  general-purpose routines.  Maybe someday will try the opposite,
//  but that smells like a deep pit of debug
modify Thing targetObj = self;
  • Enable RestrictedXXX implementations
    The solution here was to basically use Restricted components, but tweak canPutIn(obj) to allow anything by default
class ComplexSurface : ComplexComponent, RestrictedSurface
    isListedInContents = nil

    //  Define access list in parent (targetObj) property
    //  if empty, allow anything, otherwise only if in list
    //
    canPutIn(obj) {
        return
            ((targetObj.validSurfaceContents.length() == nil) || 
             (targetObj.validSurfaceContents.indexOf(obj) != nil));
    }
;
class ComplexInnerContainer : ComplexComponent, RestrictedContainer
    isListedInContents = nil

    // obvi, any global common code goes here too, ie
    //
    notifyRemove(obj) {
	    "Wow, you got it out!";
	}
    // same as above, with different property
    //
    canPutIn(obj) {
        return
            ((targetObj.validContainerContents.length() == nil) || 
             (targetObj.validContainerContents.indexOf(obj) != nil));
     }
;
class CCPerInstance : ComplexContainer // also works for ECContainer
    //  lists of acceptable contents, default is 'all objects'
    //
    validContainerContents = []
    validSurfaceContents = []
    subSurface = perInstance(new ComplexSurface())
    subContainer = perInstance(new ComplexInnerContainer())
    initializeThing() {
        subSurface.moveInto(self);
        subSurface.targetObj = self;
        subContainer.moveInto(self);
        subContainer.targetObj = self;
        inherited();
    }
;
  • Manage subLocation custom verb responses
    Nothing elegant here, just brute force, near-bare metal coding.
method ccExampleVerIobjPutIn() {
    if (someObj.isIn(self))
        illogical('The someObj prevents anything else from being put in.  ');
   	else if (gDobj is in (someSmallObj, someOtherSmallObj))
		illogical('{It dobj/he} would fit, but you would never get {the dobj/him} back out.  ');
}
ccExample : CCPerInstance  // usual vocab, name, desc omitted
    validContainerContents = [someObj, someSmallObj, someOtherSmallObj]
    initializeThing() {
        inherited;
        subContainer.setMethod(&verifyIobjPutIn, ccExampleVerIobjPutIn);
    }
;

Note if you find yourself adding a LOT of custom verb responses, you can always

ccExample : CCPerInstance  // usual vocab, name, desc omitted
    // maybe this instance doesn't need a surface?
    subSurface = nil 

    // need to customize initializeThing also, in this case
    // reverting to default
    // if not eliminating, would still need to do surface here
    //
    initializeThing() { inherited Thing; }

    // bog-standard component instantiation instead
    //
    subContainer : ComplexInnerContainer {
        iobjFor(PutIn) {
            verify() {
                logicalRank(120, 'pickThis'); inherited;
            }
        }
        iobjFor(PourInto) {
            verify() {
                illogical('Liquid would severely damage it.  ');
            }
        }
    }

Again, not claiming I’ve hit bottom yet, but more tips if wading into these thick weeds.

For posterity, the same artifact of blocked recursion happens for getOutermostRoom(), requiring the below change:

modify ComplexComponent
    getOutermostRoom() {
        /* return our container's outermost room, if we have one */
        return (targetObj != nil ? targetObj.getOutermostRoom() : self);
    }
;

Shoulda tumbled onto that a lot sooner.