To manage dialogue beats and to parse their cue paragraphs.


§1. Dialogue. This is still only partially implemented, and is aiming to implement the evolution proposal IE-0009. See the test group :dialogue to exercise problem messages in this area.

§2. Scanning the dialogue sections in pass 0. A few headings in the source text are marked as holding dialogue. Early in Inform's run, a traverse is made (see Passes through Major Nodes (in assertions)), during which the following function is called each time a heading is found.

Note that only sections, the lowest level of heading, can contain dialogue, so as soon as any other heading is reached, dialogue finishes (unless it too is so marked).

heading *dialogue_section_being_scanned = NULL;
dialogue_beat *previous_dialogue_beat = NULL;
dialogue_beat *current_dialogue_beat = NULL;

void DialogueBeats::note_heading(heading *h) {
    if (h->holds_dialogue) dialogue_section_being_scanned = h;
    else dialogue_section_being_scanned = NULL;
    previous_dialogue_beat = NULL;
    current_dialogue_beat = NULL;
    DialogueLines::clear_precursors();
}

§3. Beats. The following is called each time the cue paragraph for a new beat is found: a whole paragraph, which might, for example, read:

    (About the carriage clock; this is the horological beat.)

PN is that text, but it has already been partially parsed:

    DIALOGUE_BEAT_NT
        DIALOGUE_CLAUSE_NT "About the carriage clock"
        DIALOGUE_CLAUSE_NT "this is the horological beat"

Here we have a simple tree where the beat node has any number of child nodes, each of which is a DIALOGUE_CLAUSE_NT.

dialogue_beat *DialogueBeats::new(parse_node *PN) {
    See if we are expecting a dialogue beat3.1;
    dialogue_beat *db = CREATE(dialogue_beat);
    Initialise the beat3.3;

    previous_dialogue_beat = current_dialogue_beat;
    current_dialogue_beat = db;
    DialogueLines::clear_precursors();

    Parse the clauses just enough to classify them3.4;
    Look through the clauses for a name3.7;
    Add the beat to the world model3.9;
    return db;
}

§3.1. Note that a DIALOGUE_BEAT_NT is only made under a section marked as containing dialogue, so the internal error here should be impossible to hit.

See if we are expecting a dialogue beat3.1 =

    if (dialogue_section_being_scanned == NULL) internal_error("cue outside dialogue section");
    if (Annotations::read_int(PN, dialogue_level_ANNOT) > 0) {
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_IndentedBeat),
            "this dialogue beat seems to be indented",
            "which in dialogue would mean that it is part of something above it. "
            "But all beats (unlike lines) are free-standing, and should not be "
            "indented.");
    }

§3.2. We represent beats internally as follows:

typedef struct dialogue_beat {
    struct wording beat_name;
    struct wording scene_name;
    struct parse_node *cue_at;
    struct heading *under_heading;
    struct instance *as_instance;
    struct scene *as_scene;

    struct parse_node *immediately_after;
    struct linked_list *some_time_after;  of parse_node
    struct linked_list *some_time_before;  of parse_node
    struct linked_list *about_list;  of parse_node

    struct dialogue_line *opening_line;
    struct dialogue_beat_compilation_data compilation_data;
    CLASS_DEFINITION
} dialogue_beat;

§3.3. Initialise the beat3.3 =

    db->beat_name = EMPTY_WORDING;
    db->scene_name = EMPTY_WORDING;
    db->cue_at = PN;
    db->under_heading = dialogue_section_being_scanned;
    db->as_instance = NULL;
    db->as_scene = NULL;
    db->immediately_after = NULL;
    db->some_time_after = NEW_LINKED_LIST(parse_node);
    db->some_time_before = NEW_LINKED_LIST(parse_node);
    db->about_list = NEW_LINKED_LIST(parse_node);
    db->opening_line = NULL;
    db->compilation_data = RTDialogue::new_beat(PN, db);

§3.4. Each clause can be one of about 10 possibilities, as follows, and the wording tells us immediately which possibility it is, even early in the run. We annotate each clause with the answer. Thus we might have:

    DIALOGUE_BEAT_NT
        DIALOGUE_CLAUSE_NT "About the carriage clock" {ABOUT_DBC}
        DIALOGUE_CLAUSE_NT "this is the horological beat" {BEAT_NAME_DBC}
enum BEAT_NAME_DBC from 1
enum SCENE_NAME_DBC
enum ABOUT_DBC
enum IF_DBC
enum UNLESS_DBC
enum AFTER_DBC
enum IMMEDIATELY_AFTER_DBC
enum BEFORE_DBC
enum LATER_DBC
enum NEXT_DBC
enum PROPERTY_DBC

Parse the clauses just enough to classify them3.4 =
    for (parse_node *clause = PN->down; clause; clause = clause->next) {
        wording CW = Node::get_text(clause);
        if (Node::is(clause, DIALOGUE_CLAUSE_NT)) {
            <dialogue-beat-clause>(CW);
            Annotations::write_int(clause, dialogue_beat_clause_ANNOT, <<r>>);
        } else internal_error("damaged DIALOGUE_CUE_NT subtree");
    }

§3.5. Which is done with the following:

<dialogue-beat-clause> ::=
    this is the { ... beat } |  ==> { BEAT_NAME_DBC, - }
    this is the ... scene |     ==> { SCENE_NAME_DBC, - }
    about ... |                 ==> { ABOUT_DBC, - }
    if ... |                    ==> { IF_DBC, - }
    unless ... |                ==> { UNLESS_DBC, - }
    after ... |                 ==> { AFTER_DBC, - }
    immediately after ... |     ==> { IMMEDIATELY_AFTER_DBC, - }
    before ... |                ==> { BEFORE_DBC, - }
    later |                     ==> { LATER_DBC, - }
    next |                      ==> { NEXT_DBC, - }
    ...                         ==> { PROPERTY_DBC, - }

§3.6. It's convenient to be able to read this back in the debugging log, so:

void DialogueBeats::write_dbc(OUTPUT_STREAM, int c) {
    switch(c) {
        case BEAT_NAME_DBC: WRITE("BEAT_NAME"); break;
        case SCENE_NAME_DBC: WRITE("SCENE_NAME"); break;
        case ABOUT_DBC: WRITE("ABOUT"); break;
        case IF_DBC: WRITE("IF"); break;
        case UNLESS_DBC: WRITE("UNLESS"); break;
        case AFTER_DBC: WRITE("AFTER"); break;
        case IMMEDIATELY_AFTER_DBC: WRITE("IMMEDIATELY_AFTER"); break;
        case BEFORE_DBC: WRITE("BEFORE"); break;
        case LATER_DBC: WRITE("LATER"); break;
        case NEXT_DBC: WRITE("NEXT"); break;
        case PROPERTY_DBC: WRITE("PROPERTY"); break;
        default: WRITE("?"); break;
    }
}

§3.7. A beat can either be named this is the WHATEVER beat, or this is the WHATEVER scene, but not of course both. If the latter, we construct the beat name itself as WHATEVER beat and the name for its associated scene as WHATEVER scene.

Look through the clauses for a name3.7 =

    int dialogue_beat_name_count = 0;
    for (parse_node *clause = PN->down; clause; clause = clause->next) {
        wording CW = Node::get_text(clause);
        switch (Annotations::read_int(clause, dialogue_beat_clause_ANNOT)) {
            case BEAT_NAME_DBC:
                <dialogue-beat-clause>(CW);
                current_dialogue_beat->beat_name = GET_RW(<dialogue-beat-clause>, 1);
                dialogue_beat_name_count++;
                break;
            case SCENE_NAME_DBC:
                <dialogue-beat-clause>(CW);
                wording W = GET_RW(<dialogue-beat-clause>, 1);
                word_assemblage wa =
                    PreformUtilities::merge(<dialogue-beat-name-construction>, 0,
                        WordAssemblages::from_wording(W));
                current_dialogue_beat->beat_name = WordAssemblages::to_wording(&wa);
                wa = PreformUtilities::merge(<dialogue-beat-name-construction>, 1,
                        WordAssemblages::from_wording(W));
                current_dialogue_beat->scene_name = WordAssemblages::to_wording(&wa);
                dialogue_beat_name_count++;
                break;
        }
    }
    if (dialogue_beat_name_count > 1)
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_BeatNamedTwice),
            "this dialogue beat seems to be named more than once",
            "which is not allowed. It can be anonymous, but otherwise can only have "
            "one name (either as a beat or as a scene, and not both).");

§3.8. For the sake of translation, the above name reconstruction is done with the following Preform nonterminal:

<dialogue-beat-name-construction> ::=
    ... beat |
    ... scene

§3.9. The following creates a dialogue beat with the given name (or an invented name failing that) and makes it an instance of the kind K_dialogue_beat. This kind definitely exists, because it is created by DialogueKit, which the supervisor module has automatically added to the project on spotting that dialogue is present in the source text.

It's a little surprising, perhaps, that we do not also create the associated scene instance (if there is one). But this is for timing reasons: we want the default value of scene to be created by the Standard Rules, which will not happen until the next pass through the source text. If we create a scene instance here, it will be the first to be created, and will thus become the default.

Add the beat to the world model3.9 =

    wording W = db->beat_name;
    if (Wordings::empty(W)) {
        TEMPORARY_TEXT(faux_name)
        WRITE_TO(faux_name, "beat-%d", db->allocation_id + 1);
        W = Feeds::feed_text(faux_name);
        DISCARD_TEXT(faux_name)
    }
    if (K_dialogue_beat == NULL) internal_error("DialogueKit has not created K_dialogue_beat");
    pcalc_prop *prop = Propositions::Abstract::to_create_something(K_dialogue_beat, W);
    Assert::true(prop, CERTAIN_CE);
    db->as_instance = Instances::latest();

§4. Processing beats after pass 1. It's now a little later, and the following is called to look at each beat and parse its clauses further.

void DialogueBeats::decide_cue_sequencing(void) {
    dialogue_beat *db, *previous = NULL;
    LOOP_OVER(db, dialogue_beat) {
        current_sentence = db->cue_at;
        Create any scene instance needed4.1;
        Parse sequencing clauses4.2;
        previous = db;
    }
}

§4.1. This is unfinished business (see above), and not in fact to with beat sequencing.

Create any scene instance needed4.1 =

    if (Wordings::nonempty(db->scene_name)) {
        pcalc_prop *prop = Propositions::Abstract::to_create_something(K_scene, db->scene_name);
        Assert::true(prop, CERTAIN_CE);
        db->as_scene = Scenes::from_named_constant(Instances::latest());
    }

§4.2. But now we take care of another five clause types, all to do with the beat being performed only after or before other beats.

Parse sequencing clauses4.2 =

    int iac = 0;
    for (parse_node *clause = db->cue_at->down; clause; clause = clause->next) {
        wording CW = Node::get_text(clause);
        int c = Annotations::read_int(clause, dialogue_beat_clause_ANNOT);
        switch (c) {
            case NEXT_DBC:
                if ((previous) && (previous->under_heading == db->under_heading)) {
                    iac++;
                    db->immediately_after = Rvalues::from_dialogue_beat(previous);
                } else {
                    Issue PM_NoPreviousBeat problem4.2.1;
                }
                break;
            case LATER_DBC:
                if ((previous) && (previous->under_heading == db->under_heading)) {
                    parse_node *desc = Rvalues::from_dialogue_beat(previous);
                    ADD_TO_LINKED_LIST(desc, parse_node, db->some_time_after);
                } else {
                    Issue PM_NoPreviousBeat problem4.2.1;
                }
                break;
            case IMMEDIATELY_AFTER_DBC:
            case AFTER_DBC:
            case BEFORE_DBC: {
                <dialogue-beat-clause>(CW);
                wording A = GET_RW(<dialogue-beat-clause>, 1);
                <np-articled-list>(A);
                parse_node *AL = <<rp>>;
                DialogueBeats::parse_beat_list(c, db, AL, &iac);
                break;
            }
        }
    }
    if (iac > 1)
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_DoubleImmediateBeat),
            "this dialogue beat asks to be immediately after two or more other beats",
            "either with 'next' or 'immediately after'. It can only give one.");

§4.2.1. Issue PM_NoPreviousBeat problem4.2.1 =

    StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_NoPreviousBeat),
        "this dialogue beat asks to be performed after the previous one",
        "but in this dialogue section, there is no previous one.");

§5. Syntactically, these clauses all take articled lists: after X, Y and Z, for example. The following burrows through the resulting subtree, in which each of X, Y and Z would be an UNPARSED_NOUN_NT node.

Semantically, we can only be immediately after one beat, so we keep a count of those in order to produce a problem if there are too many. With regular "after" and "before", there are no limits.

void DialogueBeats::parse_beat_list(int c, dialogue_beat *db, parse_node *AL, int *iac) {
    if (Node::is(AL, AND_NT)) {
        DialogueBeats::parse_beat_list(c, db, AL->down, iac);
        DialogueBeats::parse_beat_list(c, db, AL->down->next, iac);
    } else if (Node::is(AL, UNPARSED_NOUN_NT)) {
        switch(c) {
            case IMMEDIATELY_AFTER_DBC: {
                wording B = GET_RW(<dialogue-beat-clause>, 1);
                parse_node *desc = DialogueBeats::parse_beat_name(B);
                if (desc) {
                    (*iac)++;
                    db->immediately_after = desc;
                }
                break;
            }
            case AFTER_DBC: {
                wording B = GET_RW(<dialogue-beat-clause>, 1);
                parse_node *desc = DialogueBeats::parse_beat_name(B);
                if (desc) ADD_TO_LINKED_LIST(desc, parse_node, db->some_time_after);
                break;
            }
            case BEFORE_DBC: {
                wording B = GET_RW(<dialogue-beat-clause>, 1);
                parse_node *desc = DialogueBeats::parse_beat_name(B);
                if (desc) ADD_TO_LINKED_LIST(desc, parse_node, db->some_time_before);
                break;
            }
        }
    }
}

parse_node *DialogueBeats::parse_beat_name(wording CW) {
    if (<s-type-expression-uncached>(CW)) {
        parse_node *desc = <<rp>>;
        kind *K = Specifications::to_kind(desc);
        if (Kinds::ne(K, K_dialogue_beat)) {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, CW);
            Problems::quote_kind(3, K);
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NotABeat));
            Problems::issue_problem_segment(
                "The dialogue beat %1 refers to another beat with '%2', but that "
                "seems to describe %3.");
            Problems::issue_problem_end();
            return NULL;
        }
        return desc;
    } else {
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, CW);
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnrecognisedBeat));
        Problems::issue_problem_segment(
            "The dialogue beat %1 refers to another beat with '%2', but that "
            "isn't something I recognise as a description.");
        Problems::issue_problem_end();
        return NULL;
    }
}

§6. Processing beats after pass 2. It's now later still. At this point all constant values have been created, and therefore we can safely parse ABOUT and PROPERTY clauses. Again, these are syntactically articled lists.

void DialogueBeats::decide_cue_topics(void) {
    dialogue_beat *db;
    LOOP_OVER(db, dialogue_beat) {
        current_sentence = db->cue_at;
        for (parse_node *clause = db->cue_at->down; clause; clause = clause->next) {
            wording CW = Node::get_text(clause);
            switch (Annotations::read_int(clause, dialogue_beat_clause_ANNOT)) {
                case ABOUT_DBC: {
                    <dialogue-beat-clause>(CW);
                    wording A = GET_RW(<dialogue-beat-clause>, 1);
                    <np-articled-list>(A);
                    parse_node *AL = <<rp>>;
                    DialogueBeats::parse_topic(db->about_list, AL);
                    break;
                }
                case PROPERTY_DBC: {
                    <dialogue-beat-clause>(CW);
                    wording A = GET_RW(<dialogue-beat-clause>, 1);
                    <np-articled-list>(A);
                    parse_node *AL = <<rp>>;
                    DialogueBeats::parse_property(db, AL);
                    break;
                }
            }
        }
    }
}

§7. Topics are picked up here. For example, about the carriage clock results in the UNPARSED_NOUN_NT node "carriage clock".

void DialogueBeats::parse_topic(linked_list *about_list, parse_node *AL) {
    if (Node::is(AL, AND_NT)) {
        DialogueBeats::parse_topic(about_list, AL->down);
        DialogueBeats::parse_topic(about_list, AL->down->next);
    } else if (Node::is(AL, UNPARSED_NOUN_NT)) {
        wording A = Node::get_text(AL);
        if (<s-type-expression-uncached>(A)) {
            parse_node *desc = <<rp>>;
            kind *K = Specifications::to_kind(desc);
            if (Kinds::Behaviour::is_subkind_of_object(K)) {
                ADD_TO_LINKED_LIST(desc, parse_node, about_list);
            } else {
                Problems::quote_source(1, current_sentence);
                Problems::quote_wording(2, A);
                Problems::quote_kind(3, K);
                StandardProblems::handmade_problem(Task::syntax_tree(),
                    _p_(PM_NotAnAboutTopic));
                Problems::issue_problem_segment(
                    "The dialogue beat %1 is apparently about '%2', but that "
                    "seems to be %3. (Dialogue can only be about objects: "
                    "people, things, rooms, that sort of stuff.)");
                Problems::issue_problem_end();
            }
        } else {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, A);
            StandardProblems::handmade_problem(Task::syntax_tree(),
                _p_(PM_UnrecognisedAboutTopic));
            Problems::issue_problem_segment(
                "The dialogue beat %1 is apparently about '%2', but that "
                "isn't something I recognise as an object. (Dialogue can "
                "only be about objects: people, things, rooms, that sort of stuff.)");
            Problems::issue_problem_end();
        }
    }
}

§8. And properties are picked up here. So recurring or spontaneous, for example, might be valid. The rule is that any text given must be either the name of an either/or property or condition which a dialogue beat can have.

void DialogueBeats::parse_property(dialogue_beat *db, parse_node *AL) {
    if (Node::is(AL, AND_NT)) {
        DialogueBeats::parse_property(db, AL->down);
        DialogueBeats::parse_property(db, AL->down->next);
    } else if (Node::is(AL, UNPARSED_NOUN_NT)) {
        inference_subject *subj = Instances::as_subject(db->as_instance);
        LOG("So K = %u\n", Instances::to_kind(db->as_instance));
        wording A = Node::get_text(AL);
        if (<s-value-uncached>(A)) {
            parse_node *val = <<rp>>;
            if (Rvalues::is_CONSTANT_construction(val, CON_property)) {
                property *prn = Rvalues::to_property(val);
                if (Properties::is_either_or(prn)) {
                    Assert that the beat has this property8.1;
                    return;
                }
            }
            if ((Specifications::is_description(val)) || (Node::is(val, TEST_VALUE_NT))) {
                Assert that the beat has this property value8.2;
                return;
            }
            LOG("Unexpected prop: $T\n", val);
        } else {
            LOG("Unrecognised prop: '%W'\n", A);
        }
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, A);
        StandardProblems::handmade_problem(Task::syntax_tree(),
            _p_(PM_UnrecognisedBeatProperty));
        Problems::issue_problem_segment(
            "The dialogue beat %1 should apparently be '%2', but that "
            "isn't something I recognise as a property which a beat can have.");
        Problems::issue_problem_end();
    }
}

§8.1. Note the introduction into the propositions of the atom dialogue-beat(x), in order to ensure that typechecking of the proposition will correctly spot that x has kind dialogue beat; without that, there would be problem messages because x would be assumed as an object.

Basically, though, this asserts the property in the same way that assertion sentences would do, and using all of the same machinery.

Assert that the beat has this property8.1 =

    pcalc_prop *prop = AdjectivalPredicates::new_atom_on_x(
        EitherOrProperties::as_adjective(prn), FALSE);
    prop = Propositions::concatenate(
        Propositions::Abstract::prop_to_set_kind(K_dialogue_beat), prop);
    Assert::true_about(prop, subj, CERTAIN_CE);

§8.2. Assert that the beat has this property value8.2 =

    pcalc_prop *prop = Descriptions::to_proposition(val);
    if (prop) {
        prop = Propositions::concatenate(
            Propositions::Abstract::prop_to_set_kind(K_dialogue_beat), prop);
        Assert::true_about(prop, subj, CERTAIN_CE);
        return;
    }

§9. So what remains to be done? Only the parsing of IF and UNLESS clauses, which take arbitrary conditions. There's no need to do that here: we can do that when compiling the runtime representation of a beat. See Dialogue (in runtime).