Source: dialects/ztree/ztt.js

/*
 * Reads z-Tree treatments (.ztt): z-Tree's binary format, as z-Tree 3 writes it (file versions
 * 16, 18 and 34 at least), into a plain object (see readTreatment). Written from z-Tree's example
 * treatments; z-Tree's format is not published.
 *
 * The format is a tree of objects. Each object is its class's name (a length-prefixed string such
 * as "EEXStage"), then its fields; in a list, each object is preceded by a 0 byte, and the list by
 * its length (4 bytes). Strings are MFC's: a length byte (or 0xff and 2 more bytes, ...) and
 * Windows-1252 text. Each class's leading fields are read exactly; the bytes after them, which hold
 * display settings this reader does not need, are skipped up to the next object, and the tree is
 * put together from the objects' classes, with the lists' lengths where their place is known.
 */

// z-Tree 5 (file version 46) names the treatment's own classes without PGX: CGame, CRole, ...
const CLASS_TAG = /^(EEX[A-Za-z]+|CPGX[A-Za-z]+|CGame|CRole|CPeriodParam|CSubjectParam)$/;

// Boxes (z-Tree's screen areas), items in them, and buttons.
const BOX_CLASSES = {
    EEXDialogWindow: 'standard',
    EEXHeaderWindow: 'header',
    EEXHelpWindow: 'help',
    EEXContainerWindow: 'container',
    EEXShowContractWindow: 'contractList',
    EEXMakeContractWindow: 'contractCreation',
    EEXHistoryWindow: 'history',
    EEXGridWindow: 'grid',
    EEXChatterbox: 'chat',
    EEXButtonWindow: 'calculator',
    EEXMessageWindow: 'message',
    EEXPlotWindow: 'plot',
    EEXMultimediaWindow: 'multimedia',
};
const ITEM_CLASSES = new Set(['EEXInfoItem']);
// Plot boxes' items (z-Tree 3's graphics).
const PLOT_CLASSES = {
    EEXPlotPoint: 'point', EEXPlotLine: 'line', EEXPlotRect: 'rect', EEXPlotPie: 'pie', EEXPlotText: 'text',
    EEXPlotAxis: 'axis', EEXPlotGraph: 'graph', EEXPlotInput: 'input',
};

// Windows-1252's characters 0x80-0x9f, where it differs from Latin-1.
const CP1252 = '€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ';
function decode(bytes) {
    let out = '';
    for (const b of bytes) out += b >= 0x80 && b <= 0x9f ? CP1252[b - 0x80] : String.fromCharCode(b);
    return out;
}

class ZttError extends Error {}

/** Reads fields from bytes buf[start, end). */
class Reader {
    constructor(buf, start = 0, end = buf.length) {
        this.buf = buf;
        this.pos = start;
        this.end = end;
    }
    get left() { return this.end - this.pos; }
    need(n) {
        if (this.pos + n > this.end) throw new ZttError('the file ends early, at byte ' + this.pos);
    }
    u8() { this.need(1); return this.buf[this.pos++]; }
    u16() { this.need(2); const v = this.buf.readUInt16LE(this.pos); this.pos += 2; return v; }
    i32() { this.need(4); const v = this.buf.readInt32LE(this.pos); this.pos += 4; return v; }
    skip(n) { this.need(n); this.pos += n; }
    /** An MFC CString. */
    str() {
        let n = this.u8();
        if (n === 0xff) {
            n = this.u16();
            if (n === 0xfffe) throw new ZttError('Unicode strings are not read yet (byte ' + this.pos + ')');
            if (n === 0xffff) n = this.i32();
        }
        this.need(n);
        const s = decode(this.buf.subarray(this.pos, this.pos + n));
        this.pos += n;
        return s.replace(/\r\n/g, '\n');
    }
    /** The strings that look like CStrings in what is left (for fields after lists, read loosely). */
    strings() {
        const out = [];
        let p = this.pos;
        while (p < this.end) {
            const n = this.buf[p];
            if (n >= 1 && n < 0xff && p + 1 + n <= this.end) {
                const bytes = this.buf.subarray(p + 1, p + 1 + n);
                if (bytes.every((c) => c >= 32 && c !== 127 || c === 9 || c === 10 || c === 13)) {
                    // Not the class of an object in the bytes (z-Tree 5's items hold CValueReal, ...).
                    const text = decode(bytes).replace(/\r\n/g, '\n');
                    if (!/^CValue[A-Za-z]*$/.test(text)) out.push(text);
                    p += 1 + n;
                    continue;
                }
            }
            p++;
        }
        return out;
    }
}

/**
 * The file's objects, in order: {cls, start (of its class name), body (where its fields start),
 * end (where the next object starts)}.
 */
function tokenize(buf) {
    const tokens = [];
    for (let p = 0; p + 2 < buf.length; p++) {
        const n = buf[p];
        if (n < 5 || n > 40 || p + 1 + n > buf.length) continue;
        const first = buf[p + 1];
        if (first !== 0x45 && first !== 0x43) continue; // E, C
        const name = buf.toString('latin1', p + 1, p + 1 + n);
        if (!CLASS_TAG.test(name)) continue;
        // The next byte must not continue the name (a longer name with this one in it).
        tokens.push({ cls: name.replace(/^C(Game|Role|PeriodParam|SubjectParam)$/, 'CPGX$1'), start: p, body: p + 1 + n });
        p += n;
    }
    // An object's fields end where the next object starts (in a list, after a 0 byte before its
    // class name; the readers read only the fields they know, so that byte does no harm).
    for (let i = 0; i < tokens.length; i++) {
        const next = tokens[i + 1];
        tokens[i].end = next == null ? buf.length : next.start;
    }
    return tokens;
}

/** Reads a z-Tree treatment (.ztt) from buf; returns its description (see the end of this function). */
function readTreatment(buf) {
    // z-Tree's text export (File → Export → Treatment), as GIMS is published: see ztext.js.
    const ztext = require('./ztext.js');
    if (ztext.isText(buf)) return ztext.readTextTreatment(buf);
    const version = buf.readInt32LE(0);
    const tokens = tokenize(buf);
    if (tokens.length === 0 || tokens[0].cls !== 'CPGXGame') {
        throw new ZttError('not a z-Tree treatment (no CPGXGame at its start)');
    }
    const warnings = [];
    const reader = (t) => new Reader(buf, t.body, t.end);
    // Bytes between a stage's timeout and its active screen's length, by file version.
    const STAGE_OPTIONS = version <= 16 ? 8 : version <= 18 ? 13 : 17;
    const BOX_EXTRA = version > 18 ? 1 : 0;

    const game = { version, tables: [], background: newStage('Background'), stages: [], periods: [], subjects: [], params: [] };
    readGameHeader(reader(tokens[0]), game);

    function newStage(name) {
        return { name, programs: [], timeout: '', options: [], showHeader: true, startIfPossible: false, timeoutAlways: false, active: [], waiting: [] };
    }

    // The tree, built from the objects' classes.
    let stage = game.background;
    let stageReader = null; // where the current stage's fields after its programs are
    let activeLeft = null;  // boxes of the current stage's active screen still to come
    let containers = [];    // open container boxes: {box, left}
    let box = null;
    let button = null;
    // The plot box being read, and its graphs open: [{list, left}] (items still to come in each).
    let plots = [];
    let plotInput = null;
    let lastPlotKind = null; // the last plot item read (its bytes after its fields differ by kind)
    let lastInputReader = null; // the last plot input's reader, after its fields
    let lastReader = null;  // the last object's reader, after its fields

    /** The fields of a stage after its programs, read where they are (after its last program). */
    function finishStageFields() {
        if (stageReader == null) return;
        const r = stageReader;
        stageReader = null;
        try {
            // From file version 46 (z-Tree 5), another list's length (its table loaders?) comes first.
            if (version >= 46) r.i32();
            stage.timeout = r.str();
            stage.options = [...buf.subarray(r.pos, r.pos + STAGE_OPTIONS)];
            r.skip(STAGE_OPTIONS);
            stageMeaning(stage, version);
            activeLeft = r.i32();
        } catch (err) {
            warnings.push('stage ' + stage.name + ': ' + err.message);
            activeLeft = Infinity;
        }
    }

    function addBox(newBox) {
        // A child of the innermost container with children to come; else a box of the stage's screens.
        while (containers.length > 0 && containers[containers.length - 1].left === 0) containers.pop();
        if (containers.length > 0) {
            containers[containers.length - 1].box.boxes.push(newBox);
            containers[containers.length - 1].left--;
        } else if (stage === game.background) {
            (newBox.type === 'header' ? stage.active : stage.waiting).push(newBox);
        } else {
            finishStageFields();
            if (activeLeft > 0) {
                stage.active.push(newBox);
                activeLeft--;
            } else {
                stage.waiting.push(newBox);
            }
        }
        if (newBox.type === 'container') containers.push({ box: newBox, left: newBox.childCount });
    }

    // From file version 34, whether a button leaves the stage: after its checkers and programs
    // (their counts, if it has none), byte 4 of its settings is 1 if it does not.
    let buttonTail = null;
    const settleButton = () => {
        const b = buttonTail;
        buttonTail = null;
        if (b == null || lastReader == null || version <= 18) return;
        try {
            const r = new Reader(buf, lastReader.pos, lastReader.end);
            if (b.programs.length === 0) r.skip(b.checkers.length === 0 ? 8 : 4);
            // Its settings: byte 2 is 1 if it leaves the stage (terminatestage), byte 4 if it does
            // not (donotterminatestage); neither, as the box and the stage have it ("Normal").
            r.skip(2);
            const leaves = r.u8();
            r.skip(1);
            const stays = r.u8();
            if (leaves === 1) b.leaveStage = true;
            else if (stays === 1) b.leaveStage = false;
        } catch (err) {
            // Not there: the box's kind decides.
        }
    };

    // A box's settings after its lists are in the bytes after its last object (its last item,
    // or its last button's last checker or program).
    let tailBox = null;
    const settleTail = () => {
        if (tailBox != null && lastReader != null) {
            tailBox.tailStrings = lastReader.strings();
            // A plot box's range, and whether it keeps the plot's proportions (one scale for x and
            // y: 1 in the 10 bytes before its range's strings, 0 to stretch the plot to the box).
            if (tailBox.type === 'plot') {
                const raw = buf.subarray(lastReader.pos, lastReader.end);
                const found = [];
                for (let p = 0; p < raw.length;) {
                    const n = raw[p];
                    if (n >= 1 && n < 0xff && p + 1 + n <= raw.length && raw.subarray(p + 1, p + 1 + n).every((c) => c >= 32 && c < 127)) {
                        found.push(p);
                        p += 1 + n;
                    } else p++;
                }
                const first = found[found.length - 4];
                if (first != null && first >= 10) tailBox.keepAspect = raw[first - 10] === 1;
            }
        }
        tailBox = null;
    };

    for (let i = 1; i < tokens.length; i++) {
        const t = tokens[i];
        const r = reader(t);
        if (buttonTail != null && !(t.cls === 'EEXChecker' || (t.cls === 'EEXProgram' && plotInput == null))) settleButton();
        if (PLOT_CLASSES[t.cls] == null && t.cls !== 'EEXProgram') closePlots(true);
        if (BOX_CLASSES[t.cls] != null || /^EEX\w*Window$/.test(t.cls) || ['EEXStage', 'CPGXPeriodParam'].includes(t.cls)) settleTail();
        try {
            if (t.cls === 'EEXDatabaseInfo') {
                // Its lifetime: the byte after its name (0 a period, 1 the treatment, 2 the session).
                const name = r.str();
                game.tables.push({ name, lifetime: ['PERIOD', 'TREATMENT', 'SESSION'][r.left > 0 ? r.u8() : 0] || 'PERIOD' });
            } else if (t.cls === 'EEXProgram') {
                const program = { table: r.str(), owner: r.str(), condition: r.str(), code: r.str() };
                if (plotInput != null) plotInput.programs.push(program);
                else if (button != null) button.programs.push(program);
                else if (box != null && box.type === 'chat') box.programs.push(program);
                else if (box != null && box.type === 'contractCreation' && button == null) box.programs.push(program);
                else {
                    stage.programs.push(program);
                    if (stage !== game.background) stageReader = r;
                }
            } else if (t.cls === 'EEXStage') {
                finishStageFields();
                stage = newStage(r.str());
                game.stages.push(stage);
                r.i32(); // its programs' count
                stageReader = r;
                activeLeft = null;
                containers = [];
                box = button = null;
            } else if (BOX_CLASSES[t.cls] != null || /^EEX\w*Window$/.test(t.cls)) {
                box = readBox(r, t.cls);
                button = null;
                plotInput = null;
                plots = box.type === 'plot' ? [{ item: box, list: box.plotItems, left: box.plotCount, root: true }] : [];
                addBox(box);
                tailBox = box;
            } else if (ITEM_CLASSES.has(t.cls)) {
                const item = readItem(r);
                if (box != null) box.items.push(item);
            } else if (PLOT_CLASSES[t.cls] != null) {
                closePlots(false);
                const item = readPlotHead(r, PLOT_CLASSES[t.cls]);
                const parent = plots[plots.length - 1];
                if (parent != null) {
                    parent.list.push(item);
                    parent.left--;
                }
                if (item.kind === 'input') plotInput = item;
                if (item.kind === 'graph') {
                    plots.push({ item, list: item.items, left: item.count });
                } else if (item.count > 0) {
                    // Its plot inputs come first, then its own fields.
                    plots.push({ item, list: item.inputs, left: item.count });
                } else {
                    readPlotFields(r, item);
                    if (item.kind === 'input') lastInputReader = r;
                }
                lastPlotKind = item.kind;
            } else if (t.cls === 'EEXChecker') {
                const checker = readChecker(r);
                if (button != null) button.checkers.push(checker);
                else if (box != null) box.items.push(checker);
            } else if (t.cls === 'EEXContractButton') {
                button = readButton(r);
                buttonTail = button;
                if (box != null) box.buttons.push(button);
            } else if (t.cls === 'CPGXPeriodParam') {
                finishStageFields();
                box = button = null;
                game.periods.push({ name: r.str(), program: r.str() });
            } else if (t.cls === 'CPGXRole') {
                // From file version 46, a role is its name only.
                const name = r.str();
                game.subjects.push({ name, index: version >= 46 ? game.subjects.length : r.i32() });
            } else if (t.cls === 'CPGXSubjectParam') {
                const param = { label: r.str() };
                // A role's own parameters (after the role, before file version 46) are its program
                // only; the table's cells (period × subject) have a label, a group and a program.
                if (version < 46 && lastToken(i).cls === 'CPGXRole') {
                    game.subjects[game.subjects.length - 1].program = param.label;
                } else if (r.left >= 5) {
                    param.group = r.i32();
                    param.program = r.str();
                    game.params.push(param);
                }
            } else {
                warnings.push('an object of class ' + t.cls + ' (at byte ' + t.start + ') is not read');
            }
        } catch (err) {
            if (!(err instanceof ZttError)) throw err;
            warnings.push(t.cls + ' at byte ' + t.start + ': ' + err.message);
        }
        lastReader = r;
    }
    settleButton();
    closePlots(true);
    settleTail();
    finishStageFields();

    function lastToken(i) { return tokens[i - 1]; }

    /** A box's fields: its header (name, placement), and its type's own fields. */
    function readBox(r, cls) {
        const b = { type: BOX_CLASSES[cls] || 'unknown', cls, name: r.str(), items: [], buttons: [], boxes: [], programs: [] };
        // Whether z-Leaf draws a frame round it.
        b.frame = r.u8() !== 0;
        b.options = [...buf.subarray(r.pos, r.pos + 8)];
        r.skip(8);
        b.condition = r.str();
        b.left = r.str();
        b.right = r.str();
        b.width = r.str();
        b.top = r.str();
        b.bottom = r.str();
        b.height = r.str();
        // From file version 34 (z-Tree 3.5?), the display condition comes after the placement.
        if (BOX_EXTRA) {
            b.first = b.condition;
            b.condition = r.str();
        }
        // From file version 46 (z-Tree 5), 4 more bytes before the kind's own fields.
        if (version >= 46) r.skip(4);
        if (b.type === 'container') {
            b.childCount = r.i32();
        } else if (b.type === 'plot') {
            b.plotItems = [];
            b.plotCount = r.i32();
        } else if (b.type === 'help') {
            b.title = r.str();
            b.text = r.str();
        } else if (b.type === 'header') {
            r.skip(4);
            b.texts = { period: r.str(), of: r.str(), trial: r.str(), time: r.str(), timeout: r.str() };
        } else if (b.type === 'chat') {
            b.table = r.str();
            b.variable = r.str();
            const rest = r.strings();
            b.condition2 = rest.find((s) => s !== b.table) || '';
            b.outputs = rest.filter((s) => s.includes('<')).slice(0, 1);
        }
        // Settings after the box's lists (contract boxes' table, condition and sorting) are read
        // later, loosely, from the bytes after its items (see settleBoxes).
        b.tailStrings = [];
        return b;
    }

    /**
     * The plot items open (graphs, and items with plot inputs), whose fields come after their
     * items: those with all their items read get their fields from the bytes after the last one
     * (innermost first). all: close the plot box's too (another box, or a stage, comes next).
     */
    function closePlots(all) {
        while (plots.length > 0 && plots[plots.length - 1].left <= 0) {
            const open = plots.pop();
            if (open.root) {
                if (!all) plots.push(open);
                break;
            }
            try {
                if (open.item.kind === 'graph') {
                    // From file version 46, a rect has 13 more bytes after its fields (before the
                    // graph's, if it is the graph's last item).
                    if (version >= 46 && lastPlotKind === 'rect' && lastReader.left >= 13) lastReader.skip(13);
                    lastPlotKind = 'graph';
                    // Its table and condition, and the line joining its points (color, width).
                    open.item.table = lastReader.str() || 'globals';
                    open.item.graphCondition = lastReader.str();
                    lastReader.str();
                    open.item.lineColor = lastReader.str();
                    open.item.lineWidth = lastReader.str();
                    if (lastReader.left >= 5) lastReader.skip(5);
                } else {
                    // Its fields, after its plot inputs: from file version 46, after the last
                    // input's 11 bytes more (if no program of it came after them).
                    if (version >= 46 && lastPlotKind === 'input' && lastReader === lastInputReader && lastReader.left >= 11) lastReader.skip(11);
                    readPlotFields(lastReader, open.item);
                    lastPlotKind = open.item.kind;
                }
            } catch (err) {
                warnings.push('plot item ' + open.item.kind + ': ' + err.message);
            }
        }
        if (all) plots = [];
    }

    /** A plot item's name, display condition, and number of plot inputs (or, for a graph, items). */
    function readPlotHead(r, kind) {
        const item = { kind, name: r.str(), condition: r.str(), count: r.i32() };
        if (kind === 'graph') item.items = [];
        else item.inputs = [];
        if (kind === 'input') item.programs = [];
        return item;
    }

    /** A plot item's own fields (expressions, as text). */
    function readPlotFields(r, item) {
        const s = () => r.str();
        switch (item.kind) {
            case 'point':
                Object.assign(item, { x: s(), y: s(), size: s(), corners: s(), rotation: s() });
                item.spokes = r.u8() !== 0;
                r.u8();
                Object.assign(item, { lineColor: s(), lineWidth: s(), fillColor: s() });
                break;
            case 'line':
                // The arrows' sizes at the start and the end, then their width (z-Tree 5's text export: arrowsize1, arrowsize2, arrowwidth).
                Object.assign(item, { x1: s(), y1: s(), x2: s(), y2: s(), arrowStart: s(), arrowEnd: s(), arrowWidth: s() });
                item.arrowFilled = r.u8() !== 0;
                r.u8();
                Object.assign(item, { lineColor: s(), lineWidth: s() });
                break;
            case 'rect':
                Object.assign(item, { xCenter: s(), yCenter: s(), width: s(), height: s(), xLeft: s(), xRight: s(), yTop: s(), yBottom: s() });
                Object.assign(item, { lineColor: s(), lineWidth: s(), fillColor: s() });
                break;
            case 'pie':
                Object.assign(item, { x: s(), y: s(), rx: s(), ry: s(), angleStart: s(), angleEnd: s(), lineColor: s(), lineWidth: s(), fillColor: s() });
                break;
            case 'text':
                Object.assign(item, { x: s(), y: s() });
                s();
                item.text = s();
                item.hAlign = r.i32();
                item.vAlign = r.i32();
                r.u8();
                item.color = s();
                s();
                item.fontSize = s();
                break;
            case 'axis':
                item.horizontal = r.u8() !== 0;
                r.u8();
                Object.assign(item, { position: s(), from: s(), to: s(), tickStep: s() });
                s(); s(); s();
                Object.assign(item, { labelStep: s(), labelLayout: s(), caption: s(), lineColor: s(), lineWidth: s() });
                break;
            case 'input':
                // What it takes: a click (0: a new record at the point), a click on an item (1: that
                // item's record), or dragging an item (2: from x0, y0 to x1, y1).
                item.action = r.i32();
                r.skip(4);
                Object.assign(item, { table: s(), xVar: s(), yVar: s() });
                // A drag's variables: after 9 bytes (10 from file version 46).
                if (r.left >= (version >= 46 ? 10 : 9)) {
                    r.skip(version >= 46 ? 10 : 9);
                    Object.assign(item, { x0Var: s(), y0Var: s(), x1Var: s(), y1Var: s() });
                }
                break;
            default:
                break;
        }
    }

    function readItem(r) {
        const item = { kind: 'item', label: r.str(), variable: r.str() };
        item.input = r.u8() !== 0;
        r.skip(5);
        item.min = r.str();
        item.max = r.str();
        item.layout = r.str();
        // A contract box's settings come after its last item, among the item's display settings.
        item.after = r.strings();
        return item;
    }

    function readChecker(r) {
        const checker = { kind: 'checker', label: r.str(), condition: r.str() };
        r.skip(4);
        checker.message = r.str();
        r.u8();
        checker.button = r.str();
        return checker;
    }

    function readButton(r) {
        r.skip(4);
        return { name: r.str(), checkers: [], programs: [], after: r.strings() };
    }

    settleBoxes(game);
    game.warnings = warnings;
    return game;
}

/** The treatment's settings before its tables: the texts shown to bankrupt subjects, ... (loosely). */
function readGameHeader(r, game) {
    const strings = r.strings();
    game.texts = strings;
}

/**
 * Contract boxes' table, condition and sorting, from the strings after their last item (where
 * z-Tree keeps them): [table, condition, sorting].
 */
function settleBoxes(game) {
    const visit = (b) => {
        if (b.type === 'plot') {
            // Its range, after its items: x left and right, y bottom and top.
            const [x1, x2, y1, y2] = (b.tailStrings || []).slice(-4);
            b.range = { xLeft: x1 || '0', xRight: x2 || '100', yBottom: y1 || '0', yTop: y2 || '100' };
        }
        if (['contractList', 'contractCreation', 'history', 'grid'].includes(b.type)) {
            const last = b.items.filter((i) => i.kind === 'item').pop();
            const after = last != null && last.after != null ? last.after : [];
            b.table = after[0] || (b.type === 'history' ? 'subjects' : 'contracts');
            // Its condition and sorting: after its buttons, or (with none) after the table's name.
            let tail = b.tailStrings || [];
            if (b.buttons.length === 0 && tail[0] === b.table) tail = tail.slice(1);
            // An owner variable (a variable's name) may come first; empty, it is not seen here.
            const isName = (x) => /^\w+$/.test(x || '');
            if (tail.length >= 2 && isName(tail[0]) && !isName(tail[1])) {
                b.owner = tail[0];
                tail = tail.slice(1);
            }
            b.tableCondition = b.type === 'contractList' ? (tail[0] || '') : '';
            b.sorting = b.type === 'contractList' ? (tail[1] || '') : '';
        }
        for (const item of b.items) delete item.after;
        for (const btn of b.buttons) delete btn.after;
        b.boxes.forEach(visit);
        delete b.tailStrings;
    };
    for (const s of [game.background, ...game.stages]) {
        s.active.forEach(visit);
        s.waiting.forEach(visit);
    }
}

/**
 * What a stage's option bytes mean, checked against z-Tree 5's text exports of the examples, and
 * against treatments z-Tree 5 saved with each setting:
 * - version 16 (z-Tree 3): options[0] bit 0, it starts as each subject gets there ("Start if
 *   possible", not "Wait for all"); bit 2, it is left after its timeout always (an auction),
 *   not only if there is no input; options[6], it shows the background's boxes (usesbg);
 * - from version 18 (z-Tree 3.5, 5): options[0] is 1 if it waits for all, options[7] 1 if it is
 *   an auction, options[11] usesbg.
 */
function stageMeaning(stage, version) {
    const o = stage.options;
    if (version <= 16) {
        stage.startIfPossible = (o[0] & 1) === 1;
        stage.showHeader = o[6] !== 0;
        stage.timeoutAlways = (o[0] & 4) === 4;
    } else {
        stage.startIfPossible = o[0] === 0;
        stage.timeoutAlways = o[7] === 1;
        stage.showHeader = o[11] !== 0;
    }
}

module.exports = { readTreatment, tokenize, Reader, ZttError, decode };