1328 lines
44 KiB
C++
1328 lines
44 KiB
C++
// Filename: cMetaInterval.cxx
|
|
// Created by: drose (27Aug02)
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
//
|
|
// PANDA 3D SOFTWARE
|
|
// Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
//
|
|
// All use of this software is subject to the terms of the revised BSD
|
|
// license. You should have received a copy of this license along
|
|
// with this source code in a file named "LICENSE."
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
|
|
#include "cMetaInterval.h"
|
|
#include "waitInterval.h"
|
|
#include "config_interval.h"
|
|
#include "indirectLess.h"
|
|
#include "indent.h"
|
|
|
|
#include <algorithm>
|
|
#include <math.h> // for log10()
|
|
#include <stdio.h> // for sprintf()
|
|
|
|
TypeHandle CMetaInterval::_type_handle;
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::Constructor
|
|
// Access: Published
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
CMetaInterval::
|
|
CMetaInterval(const string &name) :
|
|
CInterval(name, 0.0, true)
|
|
{
|
|
_precision = interval_precision;
|
|
_current_nesting_level = 0;
|
|
_next_event_index = 0;
|
|
_processing_events = false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::Destructor
|
|
// Access: Published, Virtual
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
CMetaInterval::
|
|
~CMetaInterval() {
|
|
clear_intervals();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::clear_intervals
|
|
// Access: Published
|
|
// Description: Resets the list of intervals and prepares for
|
|
// receiving a new list.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
clear_intervals() {
|
|
// Better not do this unless you have serviced all of the
|
|
// outstanding events!
|
|
bool lost_events = false;
|
|
if (!_event_queue.empty()) {
|
|
interval_cat.warning()
|
|
<< "Losing outstanding events for " << *this << "\n";
|
|
_event_queue.clear();
|
|
lost_events = true;
|
|
}
|
|
|
|
clear_events();
|
|
|
|
// Go through all of our nested intervals and remove ourselves as
|
|
// their parent.
|
|
Defs::iterator di;
|
|
for (di = _defs.begin(); di != _defs.end(); ++di) {
|
|
IntervalDef &def = (*di);
|
|
if (def._c_interval != (CInterval *)NULL) {
|
|
CInterval::Parents::iterator pi =
|
|
find(def._c_interval->_parents.begin(),
|
|
def._c_interval->_parents.end(),
|
|
this);
|
|
nassertv(pi != def._c_interval->_parents.end());
|
|
def._c_interval->_parents.erase(pi);
|
|
}
|
|
}
|
|
_defs.clear();
|
|
|
|
_current_nesting_level = 0;
|
|
_next_event_index = 0;
|
|
|
|
#ifndef NDEBUG
|
|
if (verify_intervals) {
|
|
nassertv(!lost_events);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::push_level
|
|
// Access: Published
|
|
// Description: Marks the beginning of a nested level of child
|
|
// intervals. Within the nested level, a RelativeStart
|
|
// time of RS_level_begin refers to the start of the
|
|
// level, and the first interval added within the level
|
|
// is always relative to the start of the level.
|
|
//
|
|
// The return value is the index of the def entry
|
|
// created by this push.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
push_level(const string &name, double rel_time, RelativeStart rel_to) {
|
|
nassertr(_event_queue.empty() && !_processing_events, -1);
|
|
|
|
_defs.push_back(IntervalDef());
|
|
IntervalDef &def = _defs.back();
|
|
def._type = DT_push_level;
|
|
def._ext_name = name;
|
|
def._rel_time = rel_time;
|
|
def._rel_to = rel_to;
|
|
_current_nesting_level++;
|
|
mark_dirty();
|
|
|
|
return (int)_defs.size() - 1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::add_c_interval
|
|
// Access: Published
|
|
// Description: Adds a new CInterval to the list. The interval will
|
|
// be played when the indicated time (relative to the
|
|
// given point) has been reached.
|
|
//
|
|
// The return value is the index of the def entry
|
|
// representing the new interval.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
add_c_interval(CInterval *c_interval,
|
|
double rel_time, RelativeStart rel_to) {
|
|
nassertr(_event_queue.empty() && !_processing_events, -1);
|
|
nassertr(c_interval != (CInterval *)NULL, -1);
|
|
|
|
c_interval->_parents.push_back(this);
|
|
c_interval->_ival_pcollector = PStatCollector(_ival_pcollector, c_interval->_pname);
|
|
_defs.push_back(IntervalDef());
|
|
IntervalDef &def = _defs.back();
|
|
def._type = DT_c_interval;
|
|
def._c_interval = c_interval;
|
|
def._rel_time = rel_time;
|
|
def._rel_to = rel_to;
|
|
mark_dirty();
|
|
|
|
return (int)_defs.size() - 1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::add_ext_index
|
|
// Access: Published
|
|
// Description: Adds a new external interval to the list. This
|
|
// represents some object in the external scripting
|
|
// language that has properties similar to a CInterval
|
|
// (for instance, a Python Interval object).
|
|
//
|
|
// The CMetaInterval object cannot play this external
|
|
// interval directly, but it records a placeholder for
|
|
// it and will ask the scripting language to play it
|
|
// when it is time, via is_event_ready() and related
|
|
// methods.
|
|
//
|
|
// The ext_index number itself is simply a handle that
|
|
// the scripting language makes up and associates with
|
|
// its interval object somehow. The CMetaInterval
|
|
// object does not attempt to interpret this value.
|
|
//
|
|
// The return value is the index of the def entry
|
|
// representing the new interval.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
add_ext_index(int ext_index, const string &name, double duration,
|
|
bool open_ended,
|
|
double rel_time, RelativeStart rel_to) {
|
|
nassertr(_event_queue.empty() && !_processing_events, -1);
|
|
|
|
_defs.push_back(IntervalDef());
|
|
IntervalDef &def = _defs.back();
|
|
def._type = DT_ext_index;
|
|
def._ext_index = ext_index;
|
|
def._ext_name = name;
|
|
def._ext_duration = duration;
|
|
def._ext_open_ended = open_ended;
|
|
def._rel_time = rel_time;
|
|
def._rel_to = rel_to;
|
|
mark_dirty();
|
|
|
|
return (int)_defs.size() - 1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::pop_level
|
|
// Access: Published
|
|
// Description: Finishes a level marked by a previous call to
|
|
// push_level(), and returns to the previous level.
|
|
//
|
|
// If the duration is not negative, it represents a
|
|
// phony duration to assign to the level, for the
|
|
// purposes of sequencing later intervals. Otherwise,
|
|
// the level's duration is computed based on the
|
|
// intervals within the level.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
pop_level(double duration) {
|
|
nassertr(_event_queue.empty() && !_processing_events, -1);
|
|
nassertr(_current_nesting_level > 0, -1);
|
|
|
|
_defs.push_back(IntervalDef());
|
|
IntervalDef &def = _defs.back();
|
|
def._type = DT_pop_level;
|
|
def._ext_duration = duration;
|
|
_current_nesting_level--;
|
|
mark_dirty();
|
|
|
|
return (int)_defs.size() - 1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::set_interval_start_time
|
|
// Access: Published
|
|
// Description: Adjusts the start time of the child interval with the
|
|
// given name, if found. This may be either a C++
|
|
// interval added via add_c_interval(), or an external
|
|
// interval added via add_ext_index(); the name must
|
|
// match exactly.
|
|
//
|
|
// If the interval is found, its start time is adjusted,
|
|
// and all subsequent intervals are adjusting
|
|
// accordingly, and true is returned. If a matching
|
|
// interval is not found, nothing is changed and false
|
|
// is returned.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool CMetaInterval::
|
|
set_interval_start_time(const string &name, double rel_time,
|
|
CMetaInterval::RelativeStart rel_to) {
|
|
nassertr(_event_queue.empty() && !_processing_events, false);
|
|
Defs::iterator di;
|
|
for (di = _defs.begin(); di != _defs.end(); ++di) {
|
|
IntervalDef &def = (*di);
|
|
|
|
bool match = false;
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
match = (def._c_interval->get_name() == name);
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
match = (def._ext_name == name);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (match) {
|
|
// Here's the interval.
|
|
def._rel_time = rel_time;
|
|
def._rel_to = rel_to;
|
|
mark_dirty();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::get_interval_start_time
|
|
// Access: Published
|
|
// Description: Returns the actual start time, relative to the
|
|
// beginning of the interval, of the child interval with
|
|
// the given name, if found, or -1 if the interval is
|
|
// not found.
|
|
////////////////////////////////////////////////////////////////////
|
|
double CMetaInterval::
|
|
get_interval_start_time(const string &name) const {
|
|
recompute();
|
|
Defs::const_iterator di;
|
|
for (di = _defs.begin(); di != _defs.end(); ++di) {
|
|
const IntervalDef &def = (*di);
|
|
|
|
bool match = false;
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
match = (def._c_interval->get_name() == name);
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
match = (def._ext_name == name);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (match) {
|
|
// Here's the interval.
|
|
return int_to_double_time(def._actual_begin_time);
|
|
}
|
|
}
|
|
|
|
return -1.0;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::get_interval_end_time
|
|
// Access: Published
|
|
// Description: Returns the actual end time, relative to the
|
|
// beginning of the interval, of the child interval with
|
|
// the given name, if found, or -1 if the interval is
|
|
// not found.
|
|
////////////////////////////////////////////////////////////////////
|
|
double CMetaInterval::
|
|
get_interval_end_time(const string &name) const {
|
|
recompute();
|
|
Defs::const_iterator di;
|
|
for (di = _defs.begin(); di != _defs.end(); ++di) {
|
|
const IntervalDef &def = (*di);
|
|
|
|
bool match = false;
|
|
double duration = 0.0;
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
duration = def._c_interval->get_duration();
|
|
match = (def._c_interval->get_name() == name);
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
duration = def._ext_duration;
|
|
match = (def._ext_name == name);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (match) {
|
|
// Here's the interval.
|
|
return int_to_double_time(def._actual_begin_time) + duration;
|
|
}
|
|
}
|
|
|
|
return -1.0;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::initialize
|
|
// Access: Published, Virtual
|
|
// Description: This replaces the first call to priv_step(), and indicates
|
|
// that the interval has just begun. This may be
|
|
// overridden by derived classes that need to do some
|
|
// explicit initialization on the first call.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_initialize(double t) {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_initialize, t);
|
|
return;
|
|
}
|
|
|
|
check_stopped(get_class_type(), "priv_initialize");
|
|
// It may be tempting to flush the event_queue here, but don't do
|
|
// it. Those are events that must still be serviced from some
|
|
// previous interval operation. Throwing them away would be a
|
|
// mistake.
|
|
|
|
recompute();
|
|
_next_event_index = 0;
|
|
_active.clear();
|
|
|
|
int now = double_to_int_time(t);
|
|
|
|
/*
|
|
// One special case: if we step to t == 0.0, it really means to the
|
|
// very beginning of the interval, *before* any events that occurred
|
|
// at time 0. (Most of the time, stepping to a particular time
|
|
// means *after* any events that occurred at that time.)
|
|
if (t == 0.0) {
|
|
now = -1;
|
|
}
|
|
*/
|
|
|
|
// Now look for events from the beginning up to the current time.
|
|
_processing_events = true;
|
|
ActiveEvents new_active;
|
|
while (_next_event_index < _events.size() &&
|
|
_events[_next_event_index]->_time <= now) {
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
_next_event_index++;
|
|
|
|
// Do the indicated event.
|
|
do_event_forward(event, new_active, true);
|
|
}
|
|
finish_events_forward(now, new_active);
|
|
_processing_events = false;
|
|
|
|
_curr_t = t;
|
|
_state = S_started;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::instant
|
|
// Access: Published, Virtual
|
|
// Description: This is called in lieu of priv_initialize() .. priv_step()
|
|
// .. priv_finalize(), when everything is to happen within
|
|
// one frame. The interval should initialize itself,
|
|
// then leave itself in the final state.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_instant() {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_instant);
|
|
return;
|
|
}
|
|
|
|
check_stopped(get_class_type(), "priv_instant");
|
|
recompute();
|
|
_active.clear();
|
|
|
|
// Apply all of the events. This just means we invoke "instant" for
|
|
// any end or instant event, ignoring the begin events.
|
|
_processing_events = true;
|
|
PlaybackEvents::iterator ei;
|
|
for (ei = _events.begin(); ei != _events.end(); ++ei) {
|
|
PlaybackEvent *event = (*ei);
|
|
if (event->_type != PET_begin) {
|
|
enqueue_event(event->_n, ET_instant, true, 0);
|
|
}
|
|
}
|
|
_processing_events = false;
|
|
|
|
_next_event_index = _events.size();
|
|
_curr_t = get_duration();
|
|
_state = S_final;
|
|
|
|
if (_event_queue.empty()) {
|
|
interval_done();
|
|
} else {
|
|
enqueue_done_event();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::step
|
|
// Access: Published, Virtual
|
|
// Description: Advances the time on the interval. The time may
|
|
// either increase (the normal case) or decrease
|
|
// (e.g. if the interval is being played by a slider).
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_step(double t) {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_step, t);
|
|
return;
|
|
}
|
|
|
|
check_started(get_class_type(), "priv_step");
|
|
int now = double_to_int_time(t);
|
|
|
|
/*
|
|
// One special case: if we step to t == 0.0, it really means to the
|
|
// very beginning of the interval, *before* any events that occurred
|
|
// at time 0. (Most of the time, stepping to a particular time
|
|
// means *after* any events that occurred at that time.)
|
|
if (t == 0.0) {
|
|
now = -1;
|
|
}
|
|
*/
|
|
|
|
// Now look for events between the last time we ran and the current
|
|
// time.
|
|
|
|
_processing_events = true;
|
|
if (_next_event_index < _events.size() &&
|
|
_events[_next_event_index]->_time <= now) {
|
|
// The normal case: time is increasing.
|
|
ActiveEvents new_active;
|
|
while (_next_event_index < _events.size() &&
|
|
_events[_next_event_index]->_time <= now) {
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
_next_event_index++;
|
|
|
|
// Do the indicated event.
|
|
do_event_forward(event, new_active, false);
|
|
}
|
|
|
|
finish_events_forward(now, new_active);
|
|
|
|
} else {
|
|
// A less usual case: time is decreasing.
|
|
ActiveEvents new_active;
|
|
while (_next_event_index > 0 &&
|
|
_events[_next_event_index - 1]->_time > now) {
|
|
_next_event_index--;
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
|
|
do_event_reverse(event, new_active, false);
|
|
}
|
|
|
|
finish_events_reverse(now, new_active);
|
|
}
|
|
_processing_events = false;
|
|
|
|
_curr_t = t;
|
|
_state = S_started;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::finalize
|
|
// Access: Published, Virtual
|
|
// Description: This is called when an interval is interrupted. It
|
|
// should advance the time as if priv_step() were called, and
|
|
// also perform whatever cleanup might be required.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_finalize() {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_finalize);
|
|
return;
|
|
}
|
|
|
|
double duration = get_duration();
|
|
if (_state == S_initial) {
|
|
priv_initialize(duration);
|
|
}
|
|
|
|
// Do all remaining events.
|
|
_processing_events = true;
|
|
ActiveEvents new_active;
|
|
while (_next_event_index < _events.size()) {
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
_next_event_index++;
|
|
|
|
// Do the indicated event.
|
|
do_event_forward(event, new_active, true);
|
|
}
|
|
finish_events_forward(double_to_int_time(duration), new_active);
|
|
_processing_events = false;
|
|
|
|
_curr_t = duration;
|
|
_state = S_final;
|
|
|
|
if (_event_queue.empty()) {
|
|
interval_done();
|
|
} else {
|
|
enqueue_done_event();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::reverse_initialize
|
|
// Access: Published, Virtual
|
|
// Description: Similar to priv_initialize(), but this is called when the
|
|
// interval is being played backwards; it indicates that
|
|
// the interval should start at the finishing state and
|
|
// undo any intervening intervals.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_reverse_initialize(double t) {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_reverse_initialize, t);
|
|
return;
|
|
}
|
|
|
|
check_stopped(get_class_type(), "priv_reverse_initialize");
|
|
// It may be tempting to flush the event_queue here, but don't do
|
|
// it. Those are events that must still be serviced from some
|
|
// previous interval operation. Throwing them away would be a
|
|
// mistake.
|
|
|
|
recompute();
|
|
_next_event_index = _events.size();
|
|
_active.clear();
|
|
|
|
int now = double_to_int_time(t);
|
|
|
|
/*
|
|
// One special case: if we step to t == 0.0, it really means to the
|
|
// very beginning of the interval, *before* any events that occurred
|
|
// at time 0. (Most of the time, stepping to a particular time
|
|
// means *after* any events that occurred at that time.)
|
|
if (t == 0.0) {
|
|
now = -1;
|
|
}
|
|
*/
|
|
|
|
// Now look for events from the end down to the current time.
|
|
_processing_events = true;
|
|
ActiveEvents new_active;
|
|
while (_next_event_index > 0 &&
|
|
_events[_next_event_index - 1]->_time > now) {
|
|
_next_event_index--;
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
|
|
// Do the indicated event.
|
|
do_event_reverse(event, new_active, true);
|
|
}
|
|
finish_events_reverse(now, new_active);
|
|
_processing_events = false;
|
|
|
|
_curr_t = t;
|
|
_state = S_started;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::reverse_instant
|
|
// Access: Published, Virtual
|
|
// Description: This is called in lieu of priv_reverse_initialize()
|
|
// .. priv_step() .. priv_reverse_finalize(), when everything is
|
|
// to happen within one frame. The interval should
|
|
// initialize itself, then leave itself in the initial
|
|
// state.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_reverse_instant() {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_reverse_instant);
|
|
return;
|
|
}
|
|
|
|
check_stopped(get_class_type(), "priv_reverse_instant");
|
|
recompute();
|
|
_active.clear();
|
|
|
|
// Apply all of the events. This just means we invoke "instant" for
|
|
// any end or instant event, ignoring the begin events.
|
|
_processing_events = true;
|
|
PlaybackEvents::reverse_iterator ei;
|
|
for (ei = _events.rbegin(); ei != _events.rend(); ++ei) {
|
|
PlaybackEvent *event = (*ei);
|
|
if (event->_type != PET_begin) {
|
|
enqueue_event(event->_n, ET_reverse_instant, true, 0);
|
|
}
|
|
}
|
|
_processing_events = false;
|
|
|
|
_next_event_index = 0;
|
|
_curr_t = 0.0;
|
|
_state = S_initial;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::reverse_finalize
|
|
// Access: Published, Virtual
|
|
// Description: Called generally following a priv_reverse_initialize(),
|
|
// this indicates the interval should set itself to the
|
|
// initial state.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_reverse_finalize() {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_reverse_finalize);
|
|
return;
|
|
}
|
|
|
|
if (_state == S_initial) {
|
|
priv_initialize(0.0);
|
|
}
|
|
|
|
// Do all remaining events at the beginning.
|
|
_processing_events = true;
|
|
ActiveEvents new_active;
|
|
|
|
while (_next_event_index > 0) {
|
|
_next_event_index--;
|
|
PlaybackEvent *event = _events[_next_event_index];
|
|
|
|
do_event_reverse(event, new_active, true);
|
|
}
|
|
finish_events_reverse(0, new_active);
|
|
_processing_events = false;
|
|
|
|
_curr_t = 0.0;
|
|
_state = S_initial;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::interrupt
|
|
// Access: Published, Virtual
|
|
// Description: This is called while the interval is playing to
|
|
// indicate that it is about to be interrupted; that is,
|
|
// priv_step() will not be called for a length of time. But
|
|
// the interval should remain in its current state in
|
|
// anticipation of being eventually restarted when the
|
|
// calls to priv_step() eventually resume.
|
|
//
|
|
// The purpose of this function is to allow self-running
|
|
// intervals like sound intervals to stop the actual
|
|
// sound playback during the pause.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
priv_interrupt() {
|
|
if (_processing_events) {
|
|
enqueue_self_event(ET_interrupt);
|
|
return;
|
|
}
|
|
|
|
_processing_events = true;
|
|
ActiveEvents::iterator ai;
|
|
for (ai = _active.begin(); ai != _active.end(); ++ai) {
|
|
PlaybackEvent *event = (*ai);
|
|
enqueue_event(event->_n, ET_interrupt, false);
|
|
}
|
|
_processing_events = false;
|
|
|
|
if (_state == S_started) {
|
|
_state = S_paused;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::pop_event
|
|
// Access: Published
|
|
// Description: Acknowledges that the external interval on the top of
|
|
// the queue has been extracted, and is about to be
|
|
// serviced by the scripting language. This prepares
|
|
// the interval so the next call to is_event_ready()
|
|
// will return information about the next external
|
|
// interval on the queue, if any.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
pop_event() {
|
|
#ifndef NDEBUG
|
|
nassertv(!_event_queue.empty());
|
|
const EventQueueEntry &entry = _event_queue.front();
|
|
const IntervalDef &def = _defs[entry._n];
|
|
nassertv(def._type == DT_ext_index);
|
|
#endif
|
|
_event_queue.pop_front();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::write
|
|
// Access: Published, Virtual
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
write(ostream &out, int indent_level) const {
|
|
recompute();
|
|
|
|
// How many digits of precision should we output for time?
|
|
int num_decimals = (int)ceil(log10(_precision));
|
|
int total_digits = num_decimals + 4;
|
|
static const int max_digits = 32; // totally arbitrary
|
|
nassertv(total_digits <= max_digits);
|
|
char format_str[12];
|
|
sprintf(format_str, "%%%d.%df", total_digits, num_decimals);
|
|
|
|
indent(out, indent_level) << get_name() << ":\n";
|
|
|
|
int extra_indent_level = 1;
|
|
Defs::const_iterator di;
|
|
for (di = _defs.begin(); di != _defs.end(); ++di) {
|
|
const IntervalDef &def = (*di);
|
|
char time_str[max_digits + 1];
|
|
sprintf(time_str, format_str, int_to_double_time(def._actual_begin_time));
|
|
indent(out, indent_level) << time_str;
|
|
|
|
write_event_desc(out, def, extra_indent_level);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::timeline
|
|
// Access: Published
|
|
// Description: Outputs a list of all events in the order in which
|
|
// they occur.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
timeline(ostream &out) const {
|
|
recompute();
|
|
|
|
// How many digits of precision should we output for time?
|
|
int num_decimals = (int)ceil(log10(_precision));
|
|
int total_digits = num_decimals + 4;
|
|
static const int max_digits = 32; // totally arbitrary
|
|
nassertv(total_digits <= max_digits);
|
|
char format_str[12];
|
|
sprintf(format_str, "%%%d.%df", total_digits, num_decimals);
|
|
|
|
int extra_indent_level = 0;
|
|
PlaybackEvents::const_iterator ei;
|
|
for (ei = _events.begin(); ei != _events.end(); ++ei) {
|
|
const PlaybackEvent *event = (*ei);
|
|
|
|
char time_str[max_digits + 1];
|
|
sprintf(time_str, format_str, int_to_double_time(event->_time));
|
|
out << time_str;
|
|
|
|
switch (event->_type) {
|
|
case PET_begin:
|
|
out << " begin ";
|
|
break;
|
|
case PET_end:
|
|
out << " end ";
|
|
break;
|
|
case PET_instant:
|
|
out << " instant ";
|
|
break;
|
|
}
|
|
|
|
int n = event->_n;
|
|
nassertv(n >= 0 && n < (int)_defs.size());
|
|
const IntervalDef &def = _defs[n];
|
|
|
|
write_event_desc(out, def, extra_indent_level);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::do_recompute
|
|
// Access: Protected, Virtual
|
|
// Description: Recomputes all of the events (and the duration)
|
|
// according to the set of interval defs.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
do_recompute() {
|
|
_dirty = false;
|
|
clear_events();
|
|
|
|
int n = recompute_level(0, 0, _end_time);
|
|
|
|
if (n != (int)_defs.size()) {
|
|
interval_cat.warning()
|
|
<< "CMetaInterval pushes don't match pops.\n";
|
|
}
|
|
|
|
// We do a stable_sort() to guarantee ordering of events that have
|
|
// the same start time. These must be invoked in the order in which
|
|
// they appear.
|
|
stable_sort(_events.begin(), _events.end(), IndirectLess<PlaybackEvent>());
|
|
_duration = int_to_double_time(_end_time);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::clear_events
|
|
// Access: Private
|
|
// Description: Removes all entries from the _events list.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
clear_events() {
|
|
PlaybackEvents::iterator ei;
|
|
for (ei = _events.begin(); ei != _events.end(); ++ei) {
|
|
PlaybackEvent *event = (*ei);
|
|
delete event;
|
|
}
|
|
_events.clear();
|
|
_active.clear();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::do_event_forward
|
|
// Access: Private
|
|
// Description: Process a single event in the interval, moving
|
|
// forwards in time. If the event represents a new
|
|
// begin, adds it to the new_active list; if it is an
|
|
// end, finalizes it.
|
|
//
|
|
// If is_initial is true, it is as if we are in
|
|
// initialize or finalize: instant events will be
|
|
// invoked only if they are marked open_ended.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
do_event_forward(CMetaInterval::PlaybackEvent *event,
|
|
CMetaInterval::ActiveEvents &new_active, bool is_initial) {
|
|
switch (event->_type) {
|
|
case PET_begin:
|
|
nassertv(event->_begin_event == event);
|
|
new_active.push_back(event);
|
|
break;
|
|
|
|
case PET_end:
|
|
{
|
|
// Erase the event from either the new active or the current
|
|
// active lists.
|
|
ActiveEvents::iterator ai;
|
|
ai = find(new_active.begin(), new_active.end(), event->_begin_event);
|
|
if (ai != new_active.end()) {
|
|
new_active.erase(ai);
|
|
// This interval was new this frame; we must invoke it as
|
|
// an instant event.
|
|
enqueue_event(event->_n, ET_instant, is_initial);
|
|
|
|
} else {
|
|
ai = find(_active.begin(), _active.end(), event->_begin_event);
|
|
if (ai != _active.end()) {
|
|
_active.erase(ai);
|
|
enqueue_event(event->_n, ET_finalize, is_initial);
|
|
|
|
} else {
|
|
// Hmm, this event wasn't on either list. Maybe there was a
|
|
// start event on the list whose time was less than 0.
|
|
interval_cat.error()
|
|
<< "Event " << event->_begin_event->_n << " not on active list.\n";
|
|
nassertv(false);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PET_instant:
|
|
nassertv(event->_begin_event == event);
|
|
enqueue_event(event->_n, ET_instant, is_initial);
|
|
break;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::finish_events_forward
|
|
// Access: Private
|
|
// Description: After walking through the event list and adding a
|
|
// bunch of new events to new_active, finished up by
|
|
// calling priv_step() on all of the events still in _active
|
|
// and priv_initialize() on all the events in new_active,
|
|
// then copying the events from new_active to active.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
finish_events_forward(int now, CMetaInterval::ActiveEvents &new_active) {
|
|
// Do whatever's still active.
|
|
ActiveEvents::iterator ai;
|
|
for (ai = _active.begin(); ai != _active.end(); ++ai) {
|
|
PlaybackEvent *event = (*ai);
|
|
enqueue_event(event->_n, ET_step, false, now - event->_time);
|
|
}
|
|
|
|
// Initialize whatever new intervals we came across.
|
|
for (ai = new_active.begin(); ai != new_active.end(); ++ai) {
|
|
PlaybackEvent *event = (*ai);
|
|
enqueue_event(event->_n, ET_initialize, false, now - event->_time);
|
|
_active.push_back(event);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::do_event_reverse
|
|
// Access: Private
|
|
// Description: Process a single event in the interval, moving
|
|
// backwards in time. This undoes the indicated event.
|
|
// If the event represents a new begin, adds it to the
|
|
// new_active list; if it is an end, finalizes it.
|
|
//
|
|
// If is_initial is true, it is as if we are in
|
|
// reverse_initialize or reverse_finalize: instant
|
|
// events will be invoked only if they are marked
|
|
// open_ended.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
do_event_reverse(CMetaInterval::PlaybackEvent *event,
|
|
CMetaInterval::ActiveEvents &new_active, bool is_initial) {
|
|
// Undo the indicated event.
|
|
switch (event->_type) {
|
|
case PET_begin:
|
|
{
|
|
nassertv(event->_begin_event == event);
|
|
// Erase the event from either the new active or the current
|
|
// active lists.
|
|
ActiveEvents::iterator ai;
|
|
ai = find(new_active.begin(), new_active.end(), event);
|
|
if (ai != new_active.end()) {
|
|
new_active.erase(ai);
|
|
// This interval was new this frame; we invoke it as an
|
|
// instant event.
|
|
enqueue_event(event->_n, ET_reverse_instant, is_initial);
|
|
|
|
} else {
|
|
ai = find(_active.begin(), _active.end(), event);
|
|
if (ai != _active.end()) {
|
|
_active.erase(ai);
|
|
enqueue_event(event->_n, ET_reverse_finalize, is_initial);
|
|
|
|
} else {
|
|
// Hmm, this event wasn't on either list. Maybe there was a
|
|
// stop event on the list whose time was greater than the
|
|
// total, somehow.
|
|
interval_cat.error()
|
|
<< "Event " << event->_n << " not on active list.\n";
|
|
nassertv(false);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PET_end:
|
|
new_active.push_front(event->_begin_event);
|
|
break;
|
|
|
|
case PET_instant:
|
|
nassertv(event->_begin_event == event);
|
|
enqueue_event(event->_n, ET_reverse_instant, is_initial);
|
|
break;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::finish_events_reverse
|
|
// Access: Private
|
|
// Description: After walking through the event list and adding a
|
|
// bunch of new events to new_active, finishes up by
|
|
// calling priv_step() on all of the events still in _active
|
|
// and priv_reverse_initialize() on all the events in
|
|
// new_active, then copying the events from new_active
|
|
// to active.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
finish_events_reverse(int now, CMetaInterval::ActiveEvents &new_active) {
|
|
// Do whatever's still active.
|
|
ActiveEvents::iterator ai;
|
|
for (ai = _active.begin(); ai != _active.end(); ++ai) {
|
|
PlaybackEvent *event = (*ai);
|
|
enqueue_event(event->_n, ET_step, false, now - event->_time);
|
|
}
|
|
|
|
// Initialize whatever new intervals we came across.
|
|
for (ai = new_active.begin(); ai != new_active.end(); ++ai) {
|
|
PlaybackEvent *event = (*ai);
|
|
enqueue_event(event->_n, ET_reverse_initialize, false, now - event->_time);
|
|
_active.push_front(event);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::enqueue_event
|
|
// Access: Private
|
|
// Description: Enqueues the indicated interval for invocation after
|
|
// we have finished scanning for events that need
|
|
// processing this frame.
|
|
//
|
|
// is_initial is only relevant for event types
|
|
// ET_instant or ET_reverse_instant, and indicates
|
|
// whether we are in the priv_initialize() (or
|
|
// priv_reverse_initialize()) call, and should therefore only
|
|
// invoke open-ended intervals.
|
|
//
|
|
// time is only relevant for ET_initialize,
|
|
// ET_reverse_initialize, and ET_step.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
enqueue_event(int n, CInterval::EventType event_type, bool is_initial, int time) {
|
|
nassertv(n >= 0 && n < (int)_defs.size());
|
|
const IntervalDef &def = _defs[n];
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
if (is_initial &&
|
|
(event_type == ET_instant || event_type == ET_reverse_instant) &&
|
|
!def._c_interval->get_open_ended()) {
|
|
// Ignore a non-open-ended interval that we skipped completely
|
|
// past on priv_initialize().
|
|
return;
|
|
} else {
|
|
if (_event_queue.empty()) {
|
|
// if the event queue is empty, we can process this C++
|
|
// interval immediately. We only need to defer it if there
|
|
// are external (e.g. Python) intervals in the queue that need
|
|
// to be processed first.
|
|
def._c_interval->priv_do_event(int_to_double_time(time), event_type);
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
if (is_initial &&
|
|
(event_type == ET_instant || event_type == ET_reverse_instant) &&
|
|
!def._ext_open_ended) {
|
|
// Ignore a non-open-ended interval that we skipped completely
|
|
// past on priv_initialize().
|
|
return;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
nassertv(false);
|
|
return;
|
|
}
|
|
|
|
_event_queue.push_back(EventQueueEntry(n, event_type, time));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::enqueue_self_event
|
|
// Access: Private
|
|
// Description: Enqueues a reference to *this* interval. This is
|
|
// called only when the interval is recursively
|
|
// re-entered; the request will be serviced when the
|
|
// current request is done processing.
|
|
//
|
|
// time is only relevant for ET_initialize,
|
|
// ET_reverse_initialize, and ET_step.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
enqueue_self_event(CInterval::EventType event_type, double t) {
|
|
interval_cat.info()
|
|
<< "Recursive reentry detected into " << *this << "\n";
|
|
int time = double_to_int_time(t);
|
|
_event_queue.push_back(EventQueueEntry(-1, event_type, time));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::enqueue_done_event
|
|
// Access: Private
|
|
// Description: Enqueues a special "event" that simply marks the end
|
|
// of processing of the interval; the interval's done
|
|
// event should be thrown now, if it is defined.
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
enqueue_done_event() {
|
|
_event_queue.push_back(EventQueueEntry(-2, ET_finalize, 0));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::service_event_queue
|
|
// Access: Private
|
|
// Description: Invokes whatever C++ intervals might be at the head
|
|
// of the queue, and prepares for passing an external
|
|
// interval to the scripting language.
|
|
//
|
|
// The return value is true if there remains at least
|
|
// one external event to be serviced, false if all
|
|
// events are handled.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool CMetaInterval::
|
|
service_event_queue() {
|
|
while (!_event_queue.empty()) {
|
|
nassertr(!_processing_events, true);
|
|
const EventQueueEntry &entry = _event_queue.front();
|
|
if (entry._n == -1) {
|
|
// Index -1 is a special code for *this* interval.
|
|
priv_do_event(int_to_double_time(entry._time), entry._event_type);
|
|
|
|
} else if (entry._n == -2) {
|
|
// Index -2 is a special code to indicate the interval is now
|
|
// done, and its done event should be thrown.
|
|
interval_done();
|
|
|
|
} else {
|
|
nassertr(entry._n >= 0 && entry._n < (int)_defs.size(), false);
|
|
const IntervalDef &def = _defs[entry._n];
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
// Handle the C++ event.
|
|
def._c_interval->priv_do_event(int_to_double_time(entry._time), entry._event_type);
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
// Here's an external event; leave it there and return.
|
|
return true;
|
|
|
|
default:
|
|
nassertr(false, false);
|
|
return false;
|
|
}
|
|
}
|
|
_event_queue.pop_front();
|
|
}
|
|
|
|
// No more events on the queue.
|
|
nassertr(!_processing_events, false);
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::recompute_level
|
|
// Access: Private
|
|
// Description: Recursively recomputes a complete level (delimited by
|
|
// push/pop definitions).
|
|
//
|
|
// The value n on entry refers to the first entry after
|
|
// the push; the return value will reference the
|
|
// matching pop, or an index greater than the last
|
|
// element in the array if there was no matching pop.
|
|
//
|
|
// The level_begin value indicates the begin time of
|
|
// this level. On return, level_end is filled with the
|
|
// end time of this level.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
recompute_level(int n, int level_begin, int &level_end) {
|
|
level_end = level_begin;
|
|
int previous_begin = level_begin;
|
|
int previous_end = level_begin;
|
|
|
|
while (n < (int)_defs.size() && _defs[n]._type != DT_pop_level) {
|
|
IntervalDef &def = _defs[n];
|
|
int begin_time = previous_begin;
|
|
int end_time = previous_end;
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
begin_time = get_begin_time(def, level_begin, previous_begin, previous_end);
|
|
def._actual_begin_time = begin_time;
|
|
end_time = begin_time + double_to_int_time(def._c_interval->get_duration());
|
|
|
|
if (def._c_interval->is_exact_type(WaitInterval::get_class_type())) {
|
|
// Don't bother enqueuing events for WaitIntervals; they're
|
|
// just there to fill up time.
|
|
|
|
} else {
|
|
if (begin_time == end_time) {
|
|
_events.push_back(new PlaybackEvent(begin_time, n, PET_instant));
|
|
} else {
|
|
PlaybackEvent *begin = new PlaybackEvent(begin_time, n, PET_begin);
|
|
PlaybackEvent *end = new PlaybackEvent(end_time, n, PET_end);
|
|
end->_begin_event = begin;
|
|
_events.push_back(begin);
|
|
_events.push_back(end);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
begin_time = get_begin_time(def, level_begin, previous_begin, previous_end);
|
|
def._actual_begin_time = begin_time;
|
|
end_time = begin_time + double_to_int_time(def._ext_duration);
|
|
if (begin_time == end_time) {
|
|
_events.push_back(new PlaybackEvent(begin_time, n, PET_instant));
|
|
} else {
|
|
PlaybackEvent *begin = new PlaybackEvent(begin_time, n, PET_begin);
|
|
PlaybackEvent *end = new PlaybackEvent(end_time, n, PET_end);
|
|
end->_begin_event = begin;
|
|
_events.push_back(begin);
|
|
_events.push_back(end);
|
|
}
|
|
break;
|
|
|
|
case DT_push_level:
|
|
begin_time = get_begin_time(def, level_begin, previous_begin, previous_end);
|
|
def._actual_begin_time = begin_time;
|
|
n = recompute_level(n + 1, begin_time, end_time);
|
|
break;
|
|
|
|
case DT_pop_level:
|
|
nassertr(false, _defs.size());
|
|
break;
|
|
}
|
|
|
|
previous_begin = begin_time;
|
|
previous_end = end_time;
|
|
level_end = max(level_end, end_time);
|
|
n++;
|
|
}
|
|
|
|
if (n < (int)_defs.size()) {
|
|
IntervalDef &def = _defs[n];
|
|
// If we have a pop record, check it for a phony duration.
|
|
if (def._ext_duration >= 0.0) {
|
|
level_end = level_begin + double_to_int_time(def._ext_duration);
|
|
}
|
|
|
|
// The final pop "begins" at the level end time, just for clarity
|
|
// on output.
|
|
def._actual_begin_time = level_end;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::get_begin_time
|
|
// Access: Private
|
|
// Description: Returns the integer begin time indicated by the given
|
|
// IntervalDef, given the indicated level begin,
|
|
// previous begin, and previous end times.
|
|
////////////////////////////////////////////////////////////////////
|
|
int CMetaInterval::
|
|
get_begin_time(const CMetaInterval::IntervalDef &def, int level_begin,
|
|
int previous_begin, int previous_end) {
|
|
switch (def._rel_to) {
|
|
case RS_previous_end:
|
|
return previous_end + double_to_int_time(def._rel_time);
|
|
|
|
case RS_previous_begin:
|
|
return previous_begin + double_to_int_time(def._rel_time);
|
|
|
|
case RS_level_begin:
|
|
return level_begin + double_to_int_time(def._rel_time);
|
|
}
|
|
|
|
nassertr(false, previous_end);
|
|
return previous_end;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: CMetaInterval::write_event_desc
|
|
// Access: Private
|
|
// Description: Formats an event for output, for write() or
|
|
// timeline().
|
|
////////////////////////////////////////////////////////////////////
|
|
void CMetaInterval::
|
|
write_event_desc(ostream &out, const CMetaInterval::IntervalDef &def,
|
|
int &extra_indent_level) const {
|
|
switch (def._type) {
|
|
case DT_c_interval:
|
|
indent(out, extra_indent_level)
|
|
<< *def._c_interval;
|
|
if (!def._c_interval->get_open_ended()) {
|
|
out << " (!oe)";
|
|
}
|
|
out << "\n";
|
|
break;
|
|
|
|
case DT_ext_index:
|
|
indent(out, extra_indent_level)
|
|
<< "*" << def._ext_name;
|
|
if (def._ext_duration != 0.0) {
|
|
out << " dur " << def._ext_duration;
|
|
}
|
|
if (!def._ext_open_ended) {
|
|
out << " (!oe)";
|
|
}
|
|
out<< "\n";
|
|
break;
|
|
|
|
case DT_push_level:
|
|
indent(out, extra_indent_level)
|
|
<< def._ext_name << " {\n";
|
|
extra_indent_level += 2;
|
|
break;
|
|
|
|
case DT_pop_level:
|
|
extra_indent_level -= 2;
|
|
indent(out, extra_indent_level)
|
|
<< "}\n";
|
|
break;
|
|
}
|
|
}
|