Output from distant actions without sense connectors?

Is there a concise way to get specific output from NPC actions when the NPC and player aren’t linked by a chain of sense connectors?

By default, any actions taken by an NPC in the same room as the player (or in a room connected by appropriate sense connectors to the player) are shown to the player. And any actions taken by an NPC in another room are not shown to the player. This makes sense.

But what about when you want the NPC to take some action that would be perceptible to the player regardless of (normal) sense connectors? I.e., the player and Alice are in the woods looking for something, they split up, Alice locates it, and sends up a flare. Or maybe Bob is also looking and he triggers an avalanche that the player can’t see but can hear as a distant rumble.

This kind of thing can be faked up by using daemons and so on, so that they’re the ones emitting the output, but is there a more elegant way of handling this apart from having the NPC scripting set flags that are then read by an event daemon that then displays the message?

Discounting the possibility of doing it “legitimately” by actually connecting everything with sense connectors satisfying all the requirements (which would work in principle but would be an absolute performance hog for non-trivial maps and would be a nightmare to maintain and debug).

Just for completeness, this is a kludge-ish thing that mostly does what I’m talking about.

It implements a globalOutputDaemon object that accepts messages, adds them to a queue, and then every turn outputs and resets the queue. There’s a #define that lets callers log to the queue via globalOutput() with the message to log as the single arg.

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        desc = 'sample'
        version = '1.0'
        IFID = '12345'
        showAbout() { "[This space intentionally left blank]"; }
;

globalOutputDaemon: object
        _daemon = nil
        _queue = nil

        initDaemon() {
                if(_daemon) return;
                _daemon = new Daemon(self, &runDaemon, 1);
        }
        output(msg) {
                if(!_daemon)
                        initDaemon();
                if(_queue == nil)
                        _queue = [];
                _queue += msg;
        }
        runDaemon() {
                if(_queue == nil)
                        _queue = nil;
                _queue.forEach(function(o) {
                        "<.p><<o>> ";
                });
                _queue = nil;
        }
;

#define globalOutput globalOutputDaemon.output

startRoom:      Room 'Void'
        "This is a featureless void."
        north = aliceRoom
;
+ me:     Person;

aliceRoom:      Room 'A Different Void'
        "This is also a featureless void, but a different one."
        south = startRoom
;
+ alice:        Person 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem."
        isHer = true
        isProperName = true
;
++ aliceAgenda: AgendaItem
        initiallyActive = true
        isReady = true
        invokeItem() {
                "This is a single-room message. ";
                globalOutput('This is a global message.');
        }
;

gameMain:       GameMainDef
        initialPlayerChar = me
;

Here in the demo “game”, Alice starts in a room to the north of the room the player starts in. She has an agenda that outputs “This is a single-room message.” every turn, visible only in her room, and “This is a global message.” which (via the daemon) is visible everywhere:

Void
This is a featureless void.

>z
Time passes...

This is a global message.

>n
A Different Void
This is also a featureless void, but a different one.

Alice is standing here.

This is a single-room message.

This is a global message.

This works, but seems like a bit of a hack.

Have you used callWithSenseContext yet?

callWithSenseContext(nil,nil,{: “global message. “}); should work…

And of course you could wrap that function in a simpler one that just takes a string.

That’s the suggestion in the section on agendas in Learning TADS 3, but I think there’s no way to fiddle around with the serialization that way. So you’re always going to get things in whatever arbitrary order the NPC agendas are fired in.

I guess you can do something like:

globalReportMain(msg) {
        callWithSenseContext(nil, nil, {: mainReport(msg) });
}
globalReportBefore(msg) {
        callWithSenseContext(nil, nil, {: reportBefore(msg) });
}
globalReportAfter(msg) {
        callWithSenseContext(nil, nil, {: reportAfter(msg) });
}

…but I’m not sure if that’s sufficient for what I’m trying to do.

That has the advantage of (only) using T3’s native report stuff instead of implementing a roll-your-own message queue. But I think I’m going to want to be able to twiddle the priority of individual messages. So do something like serialize the “distant” NPC-triggered messages in order of most distant to nearest (or vice versa) without having to have them all coming from a monolithic script. In other words without having one thing put everything together as one big message. And I don’t think that’s feasible using just mainReport(), reportBefore(), reportAfter(), and extraReport(), assuming everything is coming from NPC agendas that are firing in some arbitrary hash order.

Part of the problem is that I’m not 100% sure of what I need out of this, because all the NPC scripting isn’t done. But I can’t do all the scripting until I have a system for handling this kind of thing, so it’s bound to be a bit of a bodge.

You could consider having the actorOrder reflect the Actor’s distance, or make a CommandReport subclass that stores the distance of the actor. Then you could add a sort method to the transcript to show distant reports in the way you want…

The flare example is the easiest (on paper…): by its Very [1] nature, a flare is a MultiLoc, the trouble is that the PC can be everyhere, so its locationList should include the entire map, requiring also overriding its buildLocationList with one whose record the entire map.

More generally, I think that using daemons and fuses isn’t “faking”, because SenseFuse and SenseDaemon are part of both adv3 and a3Lite library, hence part of the world model, but is a philosophical reasoning, I fear…

Anyway, my opinion, and suggestion, is that SenseFuses and SenseDaemon are designed indeed for handling these types of coding problem, so pls don’t feel you’re faking…

[1] yes, pun intended.

Best regards from Italy,
dott. Piergiorgio.

actorOrder?

Anyway, the underlying issue is that I’m going to have a bunch of NPCs running around doing various things, emitting various sorts of notifications, and (I think) I’m going to want to have some way of juggling those around in a way that preserves modularity (that is, the logic for a specific NPC’s scripting/messages can live entirely in whatever the agenda/whatever that controls it) while still being able to control message ordering when necessary.

The T3 report mechanisms almost let you do this sort of thing, but they really don’t seem to be designed to give the sort of granularity of control I need. It would be easy to hammer something together to handle any one specific case (like the distance thing with the flare example) but that feels like it’s going to get unmanageable in a hurry as the number of special cases multiplies.

My first thought is some sort of message queue with explicit message priorities, although I guess another way to handle it would be to have some sort of synthetic event object (kinda like inform’s scenes) that handles all the fiddly stuff. So the NPC agendas would subscribe to some event object, so that when the event daemon fires it figures out which bits to do in which order based on who all is currently participating.

Yeah, it just seems like T3 is designed more or less entirely to handle text processing…so it seems a little odd to have to implement a message queue system on top of it.

My bad, actorOrder is a custom prop I was using in my files. I just meant you could use scheduleOrder or calcScheduleOrder to prioritize actor’s turns based on whatever distance metric you’re using, so that they would all fire in the right order.
I guess if you have a really specific report order that you want to adhere to that is governed by more than just the one factor of distance, then yeah, you probably need to make a manager object…