1 // MovieClip.cpp:  Stateful live Sprite instance, for Gnash.
2 //
3 //   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
4 //   Free Software Foundation, Inc
5 //
6 // This program is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software
18 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19 //
20 
21 #ifdef HAVE_CONFIG_H
22 #include "gnashconfig.h" // USE_SWFTREE
23 #endif
24 
25 #include "MovieClip.h"
26 
27 #include <vector>
28 #include <string>
29 #include <algorithm> // for std::swap
30 #include <boost/algorithm/string/case_conv.hpp>
31 #include <functional>
32 
33 #include "log.h"
34 #include "movie_definition.h"
35 #include "as_value.h"
36 #include "as_function.h"
37 #include "TextField.h"
38 #include "ControlTag.h"
39 #include "fn_call.h"
40 #include "movie_root.h"
41 #include "Movie.h"
42 #include "swf_event.h"
43 #include "sprite_definition.h"
44 #include "ActionExec.h"
45 #include "VM.h"
46 #include "Range2d.h" // for getBounds
47 #include "GnashException.h"
48 #include "GnashNumeric.h"
49 #include "GnashAlgorithm.h"
50 #include "URL.h"
51 #include "sound_handler.h"
52 #include "StreamProvider.h"
53 #include "LoadVariablesThread.h"
54 #include "ExecutableCode.h" // for inheritance of ConstructEvent
55 #include "DynamicShape.h" // for composition
56 #include "namedStrings.h"
57 #include "LineStyle.h"
58 #include "PlaceObject2Tag.h"
59 #include "flash/geom/Matrix_as.h"
60 #include "GnashNumeric.h"
61 #include "InteractiveObject.h"
62 #include "DisplayObjectContainer.h"
63 #include "Global_as.h"
64 #include "RunResources.h"
65 #include "Transform.h"
66 #include "ConstantPool.h" // for PoolGuard
67 
68 namespace gnash {
69 
70 //#define GNASH_DEBUG 1
71 //#define GNASH_DEBUG_TIMELINE 1
72 //#define GNASH_DEBUG_REPLACE 1
73 //#define DEBUG_DYNTEXT_VARIABLES 1
74 //#define GNASH_DEBUG_HITTEST 1
75 //#define DEBUG_LOAD_VARIABLES 1
76 
77 // Defining the following macro you'll get a DEBUG lien
78 // for each call to the drawing API, in a format which is
79 // easily re-compilable to obtain a smaller testcase
80 //#define DEBUG_DRAWING_API 1
81 
82 // Define this to make mouse entity finding verbose
83 // This includes topmostMouseEntity and findDropTarget
84 //
85 //#define DEBUG_MOUSE_ENTITY_FINDING 1
86 
87 namespace {
88     MovieClip::TextFields* textfieldVar(MovieClip::TextFieldIndex* t,
89             const ObjectURI& name);
90 }
91 
92 // Utility functors.
93 namespace {
94 
95 /// ConstructEvent, used for queuing construction
96 //
97 /// Its execution will call constructAsScriptObject()
98 /// on the target movieclip
99 ///
100 class ConstructEvent : public ExecutableCode
101 {
102 public:
103 
ConstructEvent(MovieClip * nTarget)104     explicit ConstructEvent(MovieClip* nTarget)
105         :
106         ExecutableCode(nTarget)
107     {}
108 
execute()109     virtual void execute() {
110         static_cast<MovieClip*>(target())->constructAsScriptObject();
111     }
112 
113 };
114 
115 /// Generic event  (constructed by id, invoked using notifyEvent
116 class QueuedEvent : public ExecutableCode
117 {
118 public:
119 
QueuedEvent(MovieClip * nTarget,event_id id)120     QueuedEvent(MovieClip* nTarget, event_id id)
121         :
122         ExecutableCode(nTarget),
123         _eventId(std::move(id))
124     {}
125 
execute()126     virtual void execute() {
127         // don't execute any events for destroyed DisplayObject.
128         if (!target()->isDestroyed()) {
129             static_cast<MovieClip*>(target())->notifyEvent(_eventId);
130         }
131     }
132 
133 private:
134     const event_id _eventId;
135 };
136 
137 /// Find a DisplayObject hit by the given coordinates.
138 //
139 /// This class takes care about taking masks layers into
140 /// account, but nested masks aren't properly tested yet.
141 ///
142 class MouseEntityFinder
143 {
144 public:
145 
146     /// @param wp
147     ///     Query point in world coordinate space
148     ///
149     /// @param pp
150     ///     Query point in parent coordinate space
151     ///
MouseEntityFinder(point wp,point pp)152     MouseEntityFinder(point wp, point pp)
153         :
154         _highestHiddenDepth(std::numeric_limits<int>::min()),
155         _m(nullptr),
156         _candidates(),
157         _wp(std::move(wp)),
158         _pp(std::move(pp)),
159         _checked(false)
160     {}
161 
operator ()(DisplayObject * ch)162     void operator() (DisplayObject* ch) {
163         assert(!_checked);
164         if (ch->get_depth() <= _highestHiddenDepth) {
165             if (ch->isMaskLayer()) {
166 #ifdef DEBUG_MOUSE_ENTITY_FINDING
167                 log_debug("CHECKME: nested mask in MouseEntityFinder. "
168                             "This mask is %s at depth %d outer mask masked "
169                             "up to depth %d.",
170                             ch->getTarget(), ch->get_depth(),
171                             _highestHiddenDepth);
172                 // Hiding mask still in effect...
173 #endif
174             }
175             return;
176         }
177 
178         if (ch->isMaskLayer()) {
179             if (!ch->pointInShape(_wp.x, _wp.y)) {
180 #ifdef DEBUG_MOUSE_ENTITY_FINDING
181                 log_debug("Character %s at depth %d is a mask not hitting "
182                         "the query point %g,%g and masking up to "
183                         "depth %d", ch->getTarget(), ch->get_depth(),
184                         _wp.x, _wp.y, ch->get_clip_depth());
185 #endif
186                 _highestHiddenDepth = ch->get_clip_depth();
187             }
188             else {
189 #ifdef DEBUG_MOUSE_ENTITY_FINDING
190                 log_debug("Character %s at depth %d is a mask hitting the "
191                         "query point %g,%g",
192                         ch->getTarget(), ch->get_depth(), _wp.x, _wp.y);
193 #endif
194             }
195             return;
196         }
197         if (!ch->visible()) return;
198 
199         _candidates.push_back(ch);
200     }
201 
checkCandidates()202     void checkCandidates() {
203         if (_checked) return;
204         for (Candidates::reverse_iterator i=_candidates.rbegin(),
205                         e=_candidates.rend(); i!=e; ++i) {
206             DisplayObject* ch = *i;
207             InteractiveObject* te = ch->topmostMouseEntity(_pp.x, _pp.y);
208             if (te) {
209                 _m = te;
210                 break;
211             }
212         }
213         _checked = true;
214     }
215 
getEntity()216     InteractiveObject* getEntity() {
217         checkCandidates();
218 #ifdef DEBUG_MOUSE_ENTITY_FINDING
219         if (_m) {
220             log_debug("MouseEntityFinder found DisplayObject %s (depth %d) "
221                     "hitting point %g,%g",
222                     _m->getTarget(), _m->get_depth(), _wp.x, _wp.y);
223         }
224 #endif // DEBUG_MOUSE_ENTITY_FINDING
225         return _m;
226     }
227 
228 private:
229 
230     /// Highest depth hidden by a mask
231     //
232     /// This will be -1 initially, and set
233     /// the the depth of a mask when the mask
234     /// doesn't contain the query point, while
235     /// scanning a DisplayList bottom-up
236     ///
237     int _highestHiddenDepth;
238 
239     InteractiveObject* _m;
240 
241     typedef std::vector<DisplayObject*> Candidates;
242     Candidates _candidates;
243 
244     /// Query point in world coordinate space
245     point    _wp;
246 
247     /// Query point in parent coordinate space
248     point    _pp;
249 
250     bool _checked;
251 
252 };
253 
254 /// Find the first DisplayObject whose shape contain the point
255 //
256 /// Point coordinates in world TWIPS
257 ///
258 class ShapeContainerFinder
259 {
260 public:
261 
ShapeContainerFinder(std::int32_t x,std::int32_t y)262     ShapeContainerFinder(std::int32_t x, std::int32_t y)
263         :
264         _found(false),
265         _x(x),
266         _y(y)
267     {}
268 
operator ()(const DisplayObject * ch)269     bool operator()(const DisplayObject* ch) {
270         if (ch->pointInShape(_x, _y)) {
271             _found = true;
272             return false;
273         }
274         return true;
275     }
276 
hitFound() const277     bool hitFound() const { return _found; }
278 
279 private:
280     bool _found;
281     const std::int32_t _x;
282     const std::int32_t _y;
283 };
284 
285 /// Find the first visible DisplayObject whose shape contain the point
286 //
287 /// Point coordinates in world TWIPS
288 ///
289 class VisibleShapeContainerFinder
290 {
291 public:
292 
VisibleShapeContainerFinder(std::int32_t x,std::int32_t y)293     VisibleShapeContainerFinder(std::int32_t x, std::int32_t y)
294         :
295         _found(false),
296         _x(x),
297         _y(y)
298     {}
299 
operator ()(const DisplayObject * ch)300     bool operator()(const DisplayObject* ch) {
301 
302         if (ch->pointInVisibleShape(_x, _y)) {
303             _found = true;
304             return false;
305         }
306         return true;
307     }
308 
hitFound() const309     bool hitFound() const { return _found; }
310 
311 private:
312     bool _found;
313     const std::int32_t _x;
314     const std::int32_t _y;
315 };
316 
317 /// Find the first hitable DisplayObject whose shape contain the point
318 //
319 /// Point coordinates in world TWIPS
320 ///
321 class HitableShapeContainerFinder
322 {
323 public:
HitableShapeContainerFinder(std::int32_t x,std::int32_t y)324     HitableShapeContainerFinder(std::int32_t x, std::int32_t y)
325         :
326         _found(false),
327         _x(x),
328         _y(y)
329     {}
330 
operator ()(const DisplayObject * ch)331     bool operator()(const DisplayObject* ch) {
332         if (ch->isDynamicMask()) return true;
333         if (ch->pointInShape(_x, _y)) {
334             _found = true;
335             return false;
336         }
337         return true;
338     }
339 
hitFound() const340     bool hitFound() const { return _found; }
341 
342 private:
343 
344     bool _found;
345 
346     // x position in twips.
347     const std::int32_t _x;
348 
349     // y position in twips.
350     const std::int32_t _y;
351 };
352 
353 /// A DisplayList visitor used to compute its overall bounds.
354 //
355 class BoundsFinder
356 {
357 public:
BoundsFinder(SWFRect & b)358     explicit BoundsFinder(SWFRect& b) : _bounds(b) {}
359 
operator ()(DisplayObject * ch)360     void operator()(DisplayObject* ch) {
361         // don't include bounds of unloaded DisplayObjects
362         if (ch->unloaded()) return;
363         SWFRect chb = ch->getBounds();
364         SWFMatrix m = getMatrix(*ch);
365         _bounds.expand_to_transformed_rect(m, chb);
366     }
367 
368 private:
369     SWFRect& _bounds;
370 };
371 
372 struct ReachableMarker
373 {
operator ()gnash::__anon6d4640b40211::ReachableMarker374     void operator()(DisplayObject *ch) const {
375         ch->setReachable();
376     }
377 };
378 
379 /// Find the first visible DisplayObject whose shape contain the point
380 /// and is not the DisplayObject being dragged or any of its childs
381 //
382 /// Point coordinates in world TWIPS
383 ///
384 class DropTargetFinder
385 {
386 public:
DropTargetFinder(std::int32_t x,std::int32_t y,DisplayObject * dragging)387     DropTargetFinder(std::int32_t x, std::int32_t y, DisplayObject* dragging)
388         :
389         _highestHiddenDepth(std::numeric_limits<int>::min()),
390         _x(x),
391         _y(y),
392         _dragging(dragging),
393         _dropch(nullptr),
394         _candidates(),
395         _checked(false)
396     {}
397 
operator ()(const DisplayObject * ch)398     void operator()(const DisplayObject* ch) {
399         assert(!_checked);
400         if (ch->get_depth() <= _highestHiddenDepth) {
401             if (ch->isMaskLayer()) {
402 #ifdef DEBUG_MOUSE_ENTITY_FINDING
403                 log_debug("CHECKME: nested mask in DropTargetFinder. "
404                         "This mask is %s at depth %d outer mask masked "
405                         "up to depth %d.",
406                         ch->getTarget(), ch->get_depth(), _highestHiddenDepth);
407                 // Hiding mask still in effect...
408 #endif
409             }
410             return;
411         }
412 
413         if (ch->isMaskLayer()) {
414             if (!ch->visible()) {
415                 log_debug("FIXME: invisible mask in MouseEntityFinder.");
416             }
417             if (!ch->pointInShape(_x, _y)) {
418 #ifdef DEBUG_MOUSE_ENTITY_FINDING
419                 log_debug("Character %s at depth %d is a mask not hitting "
420                         "the query point %g,%g and masking up to depth %d",
421                     ch->getTarget(), ch->get_depth(), _x, _y,
422                     ch->get_clip_depth());
423 #endif
424                 _highestHiddenDepth = ch->get_clip_depth();
425             }
426             else {
427 #ifdef DEBUG_MOUSE_ENTITY_FINDING
428                 log_debug("Character %s at depth %d is a mask "
429                             "hitting the query point %g,%g",
430                             ch->getTarget(), ch->get_depth(), _x, _y);
431 #endif
432             }
433             return;
434         }
435         _candidates.push_back(ch);
436     }
437 
checkCandidates() const438     void checkCandidates() const {
439         if (_checked) return;
440         for (Candidates::const_reverse_iterator i=_candidates.rbegin(),
441                         e=_candidates.rend(); i!=e; ++i) {
442             const DisplayObject* ch = *i;
443             const DisplayObject* dropChar =
444                 ch->findDropTarget(_x, _y, _dragging);
445             if (dropChar) {
446                 _dropch = dropChar;
447                 break;
448             }
449         }
450         _checked = true;
451     }
452 
getDropChar() const453     const DisplayObject* getDropChar() const {
454         checkCandidates();
455         return _dropch;
456     }
457 private:
458     /// Highest depth hidden by a mask
459     //
460     /// This will be -1 initially, and set
461     /// the the depth of a mask when the mask
462     /// doesn't contain the query point, while
463     /// scanning a DisplayList bottom-up
464     ///
465     int _highestHiddenDepth;
466 
467     std::int32_t _x;
468     std::int32_t _y;
469     DisplayObject* _dragging;
470     mutable const DisplayObject* _dropch;
471 
472     typedef std::vector<const DisplayObject*> Candidates;
473     Candidates _candidates;
474 
475     mutable bool _checked;
476 };
477 
478 class DisplayListVisitor
479 {
480 public:
DisplayListVisitor(KeyVisitor & v)481     DisplayListVisitor(KeyVisitor& v) : _v(v) {}
482 
operator ()(DisplayObject * ch) const483     void operator()(DisplayObject* ch) const {
484          if (!isReferenceable(*ch)) return;
485          // Don't enumerate unloaded DisplayObjects
486          if (ch->unloaded()) return;
487 
488          const ObjectURI& name = ch->get_name();
489          // Don't enumerate unnamed DisplayObjects
490          if (name.empty()) return;
491 
492          // Referenceable DisplayObject always have an object.
493          assert(getObject(ch));
494          _v(name);
495     }
496 private:
497     KeyVisitor& _v;
498 };
499 
500 } // anonymous namespace
501 
502 
MovieClip(as_object * object,const movie_definition * def,Movie * r,DisplayObject * parent)503 MovieClip::MovieClip(as_object* object, const movie_definition* def,
504         Movie* r, DisplayObject* parent)
505     :
506     DisplayObjectContainer(object, parent),
507     _def(def),
508     _swf(r),
509     _playState(PLAYSTATE_PLAY),
510     _environment(getVM(*object)),
511     _currentFrame(0),
512     m_sound_stream_id(-1),
513     _hasLooped(false),
514     _flushedOrphanedTags(false),
515     _callingFrameActions(false),
516     _lockroot(false),
517     _onLoadCalled(false)
518 {
519     assert(_swf);
520     assert(object);
521 
522     _environment.set_target(this);
523 }
524 
~MovieClip()525 MovieClip::~MovieClip()
526 {
527     stopStreamSound();
528 }
529 
530 int
getDefinitionVersion() const531 MovieClip::getDefinitionVersion() const
532 {
533     return _swf->version();
534 }
535 
536 DisplayObject*
getDisplayObjectAtDepth(int depth)537 MovieClip::getDisplayObjectAtDepth(int depth)
538 {
539     return _displayList.getDisplayObjectAtDepth(depth);
540 }
541 
542 void
queueEvent(const event_id & id,int lvl)543 MovieClip::queueEvent(const event_id& id, int lvl)
544 {
545     std::unique_ptr<ExecutableCode> event(new QueuedEvent(this, id));
546     stage().pushAction(std::move(event), lvl);
547 }
548 
549 /// This handles special properties of MovieClip.
550 //
551 /// The only genuine special properties are DisplayList members. These
552 /// are accessible as properties and are enumerated, but not ownProperties
553 /// of a MovieClip.
554 //
555 /// The TextField variables should probably be handled in a more generic
556 /// way.
557 bool
getTextFieldVariables(const ObjectURI & uri,as_value & val)558 MovieClip::getTextFieldVariables(const ObjectURI& uri, as_value& val)
559 {
560     // Try textfield variables
561     TextFields* etc = textfieldVar(_text_variables.get(), uri);
562     if (etc) {
563         for (TextFields::const_iterator i=etc->begin(), e=etc->end();
564                 i!=e; ++i) {
565 
566             TextField* tf = *i;
567             if (tf->getTextDefined()) {
568                 val = tf->get_text_value();
569                 return true;
570             }
571         }
572     }
573     return false;
574 }
575 
576 bool
get_frame_number(const as_value & frame_spec,size_t & frameno) const577 MovieClip::get_frame_number(const as_value& frame_spec, size_t& frameno) const
578 {
579     // If there is no definition, this is a dynamically-created MovieClip
580     // and has no frames.
581     if (!_def) return false;
582 
583     std::string fspecStr = frame_spec.to_string();
584 
585     as_value str(fspecStr);
586 
587     const double num = toNumber(str, getVM(*getObject(this)));
588 
589     if (!isFinite(num) || int(num) != num || num == 0) {
590         bool ret = _def->get_labeled_frame(fspecStr, frameno);
591         return ret;
592     }
593 
594     if (num < 0) return false;
595 
596     // all frame numbers > 0 are valid, but a valid frame number may still
597     // reference a non-exist frame(eg. frameno > total_frames).
598     frameno = size_t(num) - 1;
599 
600     return true;
601 }
602 
603 /// Execute the actions for the specified frame.
604 //
605 /// The frame_spec could be an integer or a string.
606 ///
607 void
call_frame_actions(const as_value & frame_spec)608 MovieClip::call_frame_actions(const as_value& frame_spec)
609 {
610     // If there is no definition, this is a dynamically-created MovieClip
611     // and has no frames.
612     if (!_def) return;
613 
614     // TODO: check to see how this can be prevented.
615     if (isDestroyed()) return;
616 
617     size_t frame_number;
618     if (!get_frame_number(frame_spec, frame_number)) {
619         // No dice.
620         IF_VERBOSE_ASCODING_ERRORS(
621             log_aserror(_("call_frame('%s') -- invalid frame"),
622                         frame_spec);
623         );
624         return;
625     }
626 
627     // Execute the ControlTag actions
628     // We set _callingFrameActions to true so that add_action_buffer
629     // will execute immediately instead of queuing them.
630     // NOTE: in case gotoFrame is executed by code in the called frame
631     //             we'll temporarly clear the _callingFrameActions flag
632     //             to properly queue actions back on the global queue.
633     //
634     _callingFrameActions = true;
635     PoolGuard poolGuard(getVM(*getObject(this)), nullptr);
636     const PlayList* playlist = _def->getPlaylist(frame_number);
637     if (playlist) {
638         PlayList::const_iterator it = playlist->begin();
639         const PlayList::const_iterator e = playlist->end();
640         for (; it != e; ++it) {
641             (*it)->executeActions(this, _displayList);
642         }
643     }
644     _callingFrameActions = false;
645 
646 }
647 
648 DisplayObject*
addDisplayListObject(DisplayObject * obj,int depth)649 MovieClip::addDisplayListObject(DisplayObject* obj, int depth)
650 {
651     // TODO: only call set_invalidated if this DisplayObject actually overrides
652     //             an existing one !
653     set_invalidated();
654     _displayList.placeDisplayObject(obj, depth);
655     obj->construct();
656     return obj;
657 }
658 
659 
660 MovieClip*
duplicateMovieClip(const std::string & newname,int depth,as_object * initObject)661 MovieClip::duplicateMovieClip(const std::string& newname, int depth,
662         as_object* initObject)
663 {
664     DisplayObject* parent_ch = parent();
665     if (!parent_ch) {
666         IF_VERBOSE_ASCODING_ERRORS(
667             log_aserror(_("Can't clone root of the movie"));
668         );
669         return nullptr;
670     }
671 
672     MovieClip* parent = parent_ch->to_movie();
673     if (!parent) {
674         IF_VERBOSE_ASCODING_ERRORS(
675             log_error(_("%s parent is not a movieclip, can't clone"),
676                 getTarget());
677         );
678         return nullptr;
679     }
680 
681     as_object* o = getObjectWithPrototype(getGlobal(*getObject(this)),
682             NSV::CLASS_MOVIE_CLIP);
683 
684     MovieClip* newmovieclip = new MovieClip(o, _def.get(), _swf, parent);
685 
686     const ObjectURI& nn = getURI(getVM(*getObject(this)), newname);
687     newmovieclip->set_name(nn);
688 
689     newmovieclip->setDynamic();
690 
691     // Copy event handlers from movieclip
692     // We should not copy 'm_action_buffer' since the
693     // 'm_method' already contains it
694     newmovieclip->set_event_handlers(get_event_handlers());
695 
696     // Copy drawable
697     newmovieclip->_drawable = _drawable;
698 
699     newmovieclip->setCxForm(getCxForm(*this));
700     newmovieclip->setMatrix(getMatrix(*this), true);
701     newmovieclip->set_ratio(get_ratio());
702     newmovieclip->set_clip_depth(get_clip_depth());
703 
704     parent->_displayList.placeDisplayObject(newmovieclip, depth);
705     newmovieclip->construct(initObject);
706 
707     return newmovieclip;
708 }
709 
710 void
queueAction(const action_buffer & action)711 MovieClip::queueAction(const action_buffer& action)
712 {
713     stage().pushAction(action, this);
714 }
715 
716 void
notifyEvent(const event_id & id)717 MovieClip::notifyEvent(const event_id& id)
718 {
719 #ifdef GNASH_DEBUG
720     log_debug("Event %s invoked for movieclip %s", id, getTarget());
721 #endif
722 
723     // We do not execute ENTER_FRAME if unloaded
724     if (id.id() == event_id::ENTER_FRAME && unloaded()) {
725 #ifdef GNASH_DEBUG
726         log_debug("Sprite %s ignored ENTER_FRAME event (is unloaded)",
727                 getTarget());
728 #endif
729         return;
730     }
731 
732     if (isButtonEvent(id) && !isEnabled()) {
733 #ifdef GNASH_DEBUG
734         log_debug("Sprite %s ignored button-like event %s as not 'enabled'",
735             getTarget(), id);
736 #endif
737         return;
738     }
739 
740     // Dispatch static event handlers (defined in PlaceObject tags).
741     std::unique_ptr<ExecutableCode> code(get_event_handler(id));
742     if (code.get()) {
743         // Dispatch.
744         code->execute();
745     }
746 
747     // Now call user-defined event handlers, but not for everything.
748 
749     // User-defined key events are never called.
750     if (isKeyEvent(id)) return;
751 
752     // user-defined onInitialize is never called
753     if (id.id() == event_id::INITIALIZE) return;
754 
755     // NOTE: user-defined onLoad is not invoked for static
756     //     clips on which no clip-events are defined.
757     //     see testsuite/misc-ming.all/action_execution_order_extend_test.swf
758     //
759     //     Note that this can't be true for movieclips
760     //     not placed by PlaceObject, see
761     //     testsuite/misc-ming.all/registerClassTest.swf
762     //
763     //     Note that this is also not true for movieclips which have
764     //     a registered class on them, see
765     //     testsuite/misc-ming.all/registerClassTest2.swf
766     //
767     //     TODO: test the case in which it's MovieClip.prototype.onLoad
768     //     defined !
769     if (id.id() == event_id::LOAD) {
770 
771         // TODO: we're likely making too much noise for nothing here,
772         // there must be some action-execution-order related problem instead....
773         // See testsuite/misc-ming.all/registerClassTest2.swf for an onLoad
774         // execution order related problem ...
775         do {
776             // we don't skip calling user-defined onLoad for top-level movies
777             if (!parent()) break;
778             // nor if there are clip-defined handler
779             if (!get_event_handlers().empty()) break;
780             // nor if it's dynamic
781             if (isDynamic()) break;
782 
783             // must be a loaded movie (loadMovie doesn't mark it as
784             // "dynamic" - should it? no, or getBytesLoaded will always
785             // return 0)
786             if (!_def) break;
787 
788             // if it has a registered class it can have an onLoad
789             // in prototype...
790             if (stage().getRegisteredClass(_def.get())) break;
791 
792 #ifdef GNASH_DEBUG
793             log_debug("Sprite %s (depth %d) won't check for user-defined "
794                         "LOAD event (is not dynamic, has a parent, "
795                         "no registered class and no clip events defined)",
796                         getTarget(), get_depth());
797 #endif
798             return;
799         } while (0);
800 
801     }
802 
803     // Call the appropriate member function.
804     sendEvent(*getObject(this), get_environment(), id.functionURI());
805 
806 }
807 
808 as_object*
pathElement(const ObjectURI & uri)809 MovieClip::pathElement(const ObjectURI& uri)
810 {
811     as_object* obj = DisplayObject::pathElement(uri);
812     if (obj) return obj;
813 
814     // See if we have a match on the display list.
815     obj = getObject(getDisplayListObject(uri));
816     if (obj) return obj;
817 
818     obj = getObject(this);
819     assert(obj);
820 
821     // See if it's a member
822     as_value tmp;
823     if (!obj->as_object::get_member(uri, &tmp)) return nullptr;
824     if (!tmp.is_object()) return nullptr;
825 
826     if (tmp.is_sprite()) {
827         return getObject(tmp.toDisplayObject(true));
828     }
829 
830     return toObject(tmp, getVM(*getObject(this)));
831 }
832 
833 bool
setTextFieldVariables(const ObjectURI & uri,const as_value & val)834 MovieClip::setTextFieldVariables(const ObjectURI& uri, const as_value& val)
835 {
836     // Try textfield variables
837     TextFields* etc = textfieldVar(_text_variables.get(), uri);
838 
839     if (!etc) return false;
840 
841     for (TextField* textfield : *etc) {
842         textfield->updateText(val.to_string(getSWFVersion(*getObject(this))));
843     }
844     return true;
845 }
846 
847 /// Remove the 'contents' of the MovieClip, but leave properties and
848 /// event handlers intact.
849 void
unloadMovie()850 MovieClip::unloadMovie()
851 {
852     LOG_ONCE(log_unimpl(_("MovieClip.unloadMovie()")));
853 }
854 
855 void
queueLoad()856 MovieClip::queueLoad()
857 {
858     if ( ! _onLoadCalled ) {
859         _onLoadCalled = true;
860         // We don't call onLoad for _root up to SWF5
861         if ( ! parent() && getSWFVersion(*getObject(this)) < 6 ) return;
862         queueEvent(event_id(event_id::LOAD),
863                     movie_root::PRIORITY_DOACTION);
864     }
865 }
866 
867 // child movieclip advance
868 void
advance()869 MovieClip::advance()
870 {
871 #ifdef GNASH_DEBUG
872     log_debug("Advance movieclip '%s' at frame %u/%u",
873         getTargetPath(), _currentFrame+1,
874         get_frame_count());
875 #endif
876 
877     assert(!unloaded());
878 
879     // call_frame should never trigger advance_movieclip
880     assert(!_callingFrameActions);
881 
882     // We might have loaded NO frames !
883     if (get_loaded_frames() == 0) {
884         IF_VERBOSE_MALFORMED_SWF(
885         LOG_ONCE( log_swferror(_("advance_movieclip: no frames loaded "
886                     "for movieclip/movie %s"), getTarget()) );
887         );
888         return;
889     }
890 
891     // Process any pending loadVariables request
892     processCompletedLoadVariableRequests();
893 
894 #ifdef GNASH_DEBUG
895     size_t frame_count = _def->get_frame_count();
896 
897     log_debug("Advance_movieclip for movieclip '%s' - frame %u/%u ",
898         getTarget(), _currentFrame+1,
899         frame_count);
900 #endif
901 
902     queueLoad();
903 
904     // I'm not sure ENTERFRAME goes in a different queue then DOACTION...
905     queueEvent(event_id(event_id::ENTER_FRAME), movie_root::PRIORITY_DOACTION);
906 
907     // Update current and next frames.
908     if (_playState == PLAYSTATE_PLAY) {
909 #ifdef GNASH_DEBUG
910         log_debug("MovieClip::advance_movieclip we're in PLAYSTATE_PLAY mode");
911 #endif
912 
913         const size_t prev_frame = _currentFrame;
914 
915 #ifdef GNASH_DEBUG
916         log_debug("on_event_load called, incrementing");
917 #endif
918         increment_frame_and_check_for_loop();
919 #ifdef GNASH_DEBUG
920         log_debug("after increment we are at frame %u/%u", _currentFrame, frame_count);
921 #endif
922 
923         // Flush any orphaned tags
924         // See https://savannah.gnu.org/bugs/index.php?33176
925         // WARNING: we might be executing these while a parser
926         //          is still pushing on it. The _hasLooped is
927         //          trying to avoid that.
928         // TODO: find a better way to ensure nobody will be pushing
929         //       to orphaned playlist while we execute it.
930         if (_currentFrame == 0 && _hasLooped) {
931 
932             const size_t frame_count = get_loaded_frames();
933             if ( frame_count != 1 || ! _flushedOrphanedTags ) {
934                 IF_VERBOSE_ACTION(
935                 log_action(_("Flushing orphaned tags in movieclip %1%. "
936                     "_currentFrame:%2%, _hasLooped:%3%, frame_count:%4%"),
937                     getTargetPath(), _currentFrame, _hasLooped, frame_count)
938                 );
939                 _flushedOrphanedTags = true;
940                 executeFrameTags(frame_count, _displayList,
941                     SWF::ControlTag::TAG_DLIST |
942                     SWF::ControlTag::TAG_ACTION);
943             }
944         }
945 
946         // Execute the current frame's tags.
947         // First time executeFrameTags(0) executed in dlist.cpp(child) or
948         // SWFMovieDefinition(root)
949         if (_currentFrame != prev_frame) {
950 
951             if (_currentFrame == 0 && _hasLooped) {
952 #ifdef GNASH_DEBUG
953                 log_debug("Jumping back to frame 0 of movieclip %s",
954                         getTarget());
955 #endif
956                 restoreDisplayList(0); // seems OK to me.
957             }
958             else {
959 #ifdef GNASH_DEBUG
960                 log_debug("Executing frame%d (0-based) tags of movieclip "
961                             "%s", _currentFrame, getTarget());
962 #endif
963                 // Make sure _currentFrame is 0-based during execution of
964                 // DLIST tags
965                 executeFrameTags(_currentFrame, _displayList,
966                         SWF::ControlTag::TAG_DLIST |
967                         SWF::ControlTag::TAG_ACTION);
968             }
969         }
970 
971     }
972 #ifdef GNASH_DEBUG
973     else {
974         log_debug("MovieClip::advance_movieclip we're in STOP mode");
975     }
976 #endif
977 }
978 
979 void
execute_init_action_buffer(const action_buffer & a,int cid)980 MovieClip::execute_init_action_buffer(const action_buffer& a, int cid)
981 {
982     assert(cid >= 0);
983 
984     if (_swf->initializeCharacter(cid)) {
985 #ifdef GNASH_DEBUG
986         log_debug("Queuing init actions for DisplayObject %1% "
987                     "in frame %2% of MovieClip %3%",
988                 cid, _currentFrame, getTarget());
989 #endif
990         std::unique_ptr<ExecutableCode> code(new GlobalCode(a, this));
991 
992         stage().pushAction(std::move(code), movie_root::PRIORITY_INIT);
993     }
994     else {
995 #ifdef GNASH_DEBUG
996         log_debug("Init actions for DisplayObject %1% already executed", cid);
997 #endif
998     }
999 }
1000 
1001 void
execute_action(const action_buffer & ab)1002 MovieClip::execute_action(const action_buffer& ab)
1003 {
1004     ActionExec exec(ab, _environment);
1005     exec();
1006 }
1007 
1008 void
restoreDisplayList(size_t tgtFrame)1009 MovieClip::restoreDisplayList(size_t tgtFrame)
1010 {
1011     // This is not tested as usable for jump-forwards (yet)...
1012     // TODO: I guess just moving here the code currently in goto_frame
1013     //             for jump-forwards would do
1014     assert(tgtFrame <= _currentFrame);
1015 
1016     DisplayList tmplist;
1017     for (size_t f = 0; f < tgtFrame; ++f) {
1018         _currentFrame = f;
1019         executeFrameTags(f, tmplist, SWF::ControlTag::TAG_DLIST);
1020     }
1021 
1022     // Execute both action tags and DLIST tags of the target frame
1023     _currentFrame = tgtFrame;
1024     executeFrameTags(tgtFrame, tmplist, SWF::ControlTag::TAG_DLIST |
1025                                         SWF::ControlTag::TAG_ACTION);
1026 
1027     _displayList.mergeDisplayList(tmplist, *this);
1028 }
1029 
1030 // 0-based frame number !
1031 void
executeFrameTags(size_t frame,DisplayList & dlist,int typeflags)1032 MovieClip::executeFrameTags(size_t frame, DisplayList& dlist, int typeflags)
1033 {
1034     // If there is no definition, this is a dynamically-created MovieClip
1035     // and has no frames.
1036     if (!_def) return;
1037     if (isDestroyed()) return;
1038 
1039     assert(typeflags);
1040 
1041     const PlayList* playlist = _def->getPlaylist(frame);
1042     if (playlist) {
1043 
1044         IF_VERBOSE_ACTION(
1045             // Use 1-based frame numbers
1046             log_action(_("Executing %d tags in frame %d/%d of movieclip %s"),
1047                 playlist->size(), frame + 1, get_frame_count(),
1048                 getTargetPath());
1049         );
1050 
1051         // Generally tags should be executed in the order they are found in.
1052         for (const auto& item : *playlist) {
1053 
1054             if (typeflags & SWF::ControlTag::TAG_DLIST) {
1055                 item->executeState(this, dlist);
1056             }
1057 
1058             if (typeflags & SWF::ControlTag::TAG_ACTION) {
1059                 item->executeActions(this, _displayList);
1060             }
1061         }
1062     }
1063 }
1064 
1065 void
goto_frame(size_t target_frame_number)1066 MovieClip::goto_frame(size_t target_frame_number)
1067 {
1068 #if defined(DEBUG_GOTOFRAME) || defined(GNASH_DEBUG_TIMELINE)
1069     log_debug("movieclip %s ::goto_frame(%d) - current frame is %d",
1070         getTargetPath(), target_frame_number, _currentFrame);
1071 #endif
1072 
1073     // goto_frame stops by default.
1074     // ActionGotoFrame tells the movieClip to go to the target frame
1075     // and stop at that frame.
1076     setPlayState(PLAYSTATE_STOP);
1077 
1078     if (target_frame_number > _def->get_frame_count() - 1) {
1079 
1080         target_frame_number = _def->get_frame_count() - 1;
1081 
1082         if (!_def->ensure_frame_loaded(target_frame_number + 1)) {
1083             log_error(_("Target frame of a gotoFrame(%d) was never loaded, "
1084                         "although frame count in header (%d) said we "
1085                         "should have found it"),
1086                         target_frame_number+1, _def->get_frame_count());
1087             return;
1088         }
1089 
1090         // Just set _currentframe and return.
1091         _currentFrame = target_frame_number;
1092 
1093         // don't push actions, already tested.
1094         return;
1095     }
1096 
1097     if (target_frame_number == _currentFrame) {
1098         // don't push actions
1099         return;
1100     }
1101 
1102     // Unless the target frame is the next one, stop playback of soundstream
1103     if (target_frame_number != _currentFrame + 1) {
1104         stopStreamSound();
1105     }
1106 
1107     const size_t loaded_frames = get_loaded_frames();
1108 
1109     // target_frame_number is 0-based, get_loaded_frames() is 1-based
1110     // so in order to goto_frame(3) loaded_frames must be at least 4
1111     // if goto_frame(4) is called, and loaded_frames is 4 we're jumping
1112     // forward
1113     if (target_frame_number >= loaded_frames) {
1114         IF_VERBOSE_ASCODING_ERRORS(
1115             log_aserror(_("GotoFrame(%d) targets a yet "
1116             "to be loaded frame (%d). "
1117             "We'll wait for it but a more correct form "
1118             "is explicitly using WaitForFrame instead"),
1119             target_frame_number+1,
1120             loaded_frames);
1121 
1122         );
1123         if (!_def->ensure_frame_loaded(target_frame_number + 1)) {
1124             log_error(_("Target frame of a gotoFrame(%d) was never loaded, "
1125                         "although frame count in header (%d) said we should"
1126                         " have found it"),
1127                         target_frame_number + 1, _def->get_frame_count());
1128             return;
1129         }
1130     }
1131 
1132     // Construct the DisplayList of the target frame
1133     if (target_frame_number < _currentFrame) {
1134 
1135         // Go backward to a previous frame
1136         // NOTE: just in case we're being called by code in a called frame
1137         // we'll backup and resume the _callingFrameActions flag
1138         bool callingFrameActionsBackup = _callingFrameActions;
1139         _callingFrameActions = false;
1140 
1141         // restoreDisplayList takes care of properly setting the
1142         // _currentFrame variable
1143         restoreDisplayList(target_frame_number);
1144         assert(_currentFrame == target_frame_number);
1145         _callingFrameActions = callingFrameActionsBackup;
1146     }
1147     else {
1148         // Go forward to a later frame
1149         // We'd immediately return if target_frame_number == _currentFrame
1150         assert(target_frame_number > _currentFrame);
1151         while (++_currentFrame < target_frame_number) {
1152             //for (size_t f = _currentFrame+1; f<target_frame_number; ++f)
1153             // Second argument requests that only "DisplayList" tags
1154             // are executed. This means NO actions will be
1155             // pushed on m_action_list.
1156             executeFrameTags(_currentFrame, _displayList,
1157                     SWF::ControlTag::TAG_DLIST);
1158         }
1159         assert(_currentFrame == target_frame_number);
1160 
1161         // Now execute target frame tags (queuing actions)
1162         // NOTE: just in case we're being called by code in a called frame
1163         //             we'll backup and resume the _callingFrameActions flag
1164         bool callingFrameActionsBackup = _callingFrameActions;
1165         _callingFrameActions = false;
1166         executeFrameTags(target_frame_number, _displayList,
1167                 SWF::ControlTag::TAG_DLIST | SWF::ControlTag::TAG_ACTION);
1168         _callingFrameActions = callingFrameActionsBackup;
1169     }
1170 
1171     assert(_currentFrame == target_frame_number);
1172 }
1173 
1174 bool
goto_labeled_frame(const std::string & label)1175 MovieClip::goto_labeled_frame(const std::string& label)
1176 {
1177     // If there is no definition, this is a dynamically-created MovieClip
1178     // and has no frames. (We are also probably not called in this case).
1179     if (!_def) return false;
1180 
1181     size_t target_frame;
1182     if (_def->get_labeled_frame(label, target_frame)) {
1183         goto_frame(target_frame);
1184         return true;
1185     }
1186 
1187     IF_VERBOSE_MALFORMED_SWF(
1188         log_swferror(_("MovieClip::goto_labeled_frame('%s') "
1189             "unknown label"), label);
1190     );
1191     return false;
1192 }
1193 
1194 void
draw(Renderer & renderer,const Transform & xform)1195 MovieClip::draw(Renderer& renderer, const Transform& xform)
1196 {
1197     const DisplayObject::MaskRenderer mr(renderer, *this);
1198 
1199     _drawable.finalize();
1200     _drawable.display(renderer, xform);
1201     _displayList.display(renderer, xform);
1202 }
1203 
1204 void
display(Renderer & renderer,const Transform & base)1205 MovieClip::display(Renderer& renderer, const Transform& base)
1206 {
1207     // Note: DisplayList::display() will take care of the visibility checking.
1208     //
1209     // Whether a DisplayObject should be rendered or not is dependent
1210     // on its parent: i.e. if its parent is a mask, this DisplayObject
1211     // should be rendered to the mask buffer even it is invisible.
1212 
1213     // Draw everything with our own transform.
1214     const Transform xform = base * transform();
1215     draw(renderer, xform);
1216     clear_invalidated();
1217 }
1218 
omit_display()1219 void MovieClip::omit_display()
1220 {
1221     if (childInvalidated()) _displayList.omit_display();
1222     clear_invalidated();
1223 }
1224 
1225 void
attachCharacter(DisplayObject & newch,int depth,as_object * initObj)1226 MovieClip::attachCharacter(DisplayObject& newch, int depth, as_object* initObj)
1227 {
1228     _displayList.placeDisplayObject(&newch, depth);
1229     newch.construct(initObj);
1230 }
1231 
1232 DisplayObject*
add_display_object(const SWF::PlaceObject2Tag * tag,DisplayList & dlist)1233 MovieClip::add_display_object(const SWF::PlaceObject2Tag* tag,
1234         DisplayList& dlist)
1235 {
1236     // If this MovieClip has no definition, it should also have no ControlTags,
1237     // and this shouldn't be called.
1238     assert(_def);
1239     assert(tag);
1240 
1241     // No tags should ever be executed on destroyed MovieClips.
1242     assert(!isDestroyed());
1243 
1244     SWF::DefinitionTag* cdef = _def->getDefinitionTag(tag->getID());
1245     if (!cdef) {
1246         IF_VERBOSE_MALFORMED_SWF(
1247             log_swferror(_("MovieClip::add_display_object(): "
1248                     "unknown cid = %d"), tag->getID());
1249         );
1250         return nullptr;
1251     }
1252 
1253     DisplayObject* existing_char = dlist.getDisplayObjectAtDepth(tag->getDepth());
1254 
1255     if (existing_char) return nullptr;
1256 
1257     Global_as& gl = getGlobal(*getObject(this));
1258     VM& vm = getVM(*getObject(this));
1259     DisplayObject* ch = cdef->createDisplayObject(gl, this);
1260 
1261     if (tag->hasName()) ch->set_name(getURI(vm, tag->getName()));
1262     else if (isReferenceable(*ch)) {
1263         const ObjectURI& instance_name = getNextUnnamedInstanceName();
1264         ch->set_name(instance_name);
1265     }
1266 
1267     if (tag->hasBlendMode()) {
1268         std::uint8_t bm = tag->getBlendMode();
1269         ch->setBlendMode(static_cast<DisplayObject::BlendMode>(bm));
1270     }
1271 
1272     // Attach event handlers (if any).
1273     const SWF::PlaceObject2Tag::EventHandlers& event_handlers =
1274         tag->getEventHandlers();
1275 
1276     for (size_t i = 0, n = event_handlers.size(); i < n; ++i) {
1277         const swf_event& ev = event_handlers[i];
1278         ch->add_event_handler(ev.event(), ev.action());
1279     }
1280 
1281     // TODO: check if we should check those has_xxx flags first.
1282     ch->setCxForm(tag->getCxform());
1283     ch->setMatrix(tag->getMatrix(), true); // update caches
1284     ch->set_ratio(tag->getRatio());
1285     ch->set_clip_depth(tag->getClipDepth());
1286 
1287     dlist.placeDisplayObject(ch, tag->getDepth());
1288     ch->construct();
1289     return ch;
1290 }
1291 
1292 void
move_display_object(const SWF::PlaceObject2Tag * tag,DisplayList & dlist)1293 MovieClip::move_display_object(const SWF::PlaceObject2Tag* tag, DisplayList& dlist)
1294 {
1295     std::uint16_t ratio = tag->getRatio();
1296     // clip_depth is not used in MOVE tag(at least no related tests).
1297     dlist.moveDisplayObject(
1298         tag->getDepth(),
1299         tag->hasCxform() ? &tag->getCxform() : nullptr,
1300         tag->hasMatrix() ? &tag->getMatrix() : nullptr,
1301         tag->hasRatio() ? &ratio : nullptr);
1302 }
1303 
1304 void
replace_display_object(const SWF::PlaceObject2Tag * tag,DisplayList & dlist)1305 MovieClip::replace_display_object(const SWF::PlaceObject2Tag* tag,
1306         DisplayList& dlist)
1307 {
1308     // A MovieClip without a definition cannot have any ControlTags, so this
1309     // should not be called.
1310     assert(_def);
1311     assert(tag != nullptr);
1312 
1313     const std::uint16_t id = tag->getID();
1314 
1315     SWF::DefinitionTag* cdef = _def->getDefinitionTag(id);
1316     if (!cdef) {
1317         log_error(_("movieclip::replace_display_object(): "
1318             "unknown cid = %d"), id);
1319         return;
1320     }
1321     assert(cdef);
1322 
1323     DisplayObject* existing_char = dlist.getDisplayObjectAtDepth(tag->getDepth());
1324 
1325     if (!existing_char) {
1326         log_error(_("MovieClip::replace_display_object: could not "
1327                     "find any DisplayObject at depth %d"), tag->getDepth());
1328         return;
1329     }
1330 
1331     // if the existing DisplayObject is not a shape, move it instead
1332     // of replacing.
1333     if (isReferenceable(*existing_char)) {
1334         move_display_object(tag, dlist);
1335         return;
1336     }
1337 
1338     Global_as& gl = getGlobal(*getObject(this));
1339     DisplayObject* ch = cdef->createDisplayObject(gl, this);
1340 
1341 
1342     // TODO: check if we can drop this for REPLACE!
1343     // should we rename the DisplayObject when it's REPLACE tag?
1344     if (tag->hasName()) {
1345         VM& vm = getVM(*getObject(this));
1346         ch->set_name(getURI(vm, tag->getName()));
1347     }
1348     else if (isReferenceable(*ch)) {
1349         ch->set_name(getNextUnnamedInstanceName());
1350     }
1351     if (tag->hasRatio()) {
1352         ch->set_ratio(tag->getRatio());
1353     }
1354     if (tag->hasCxform()) {
1355         ch->setCxForm(tag->getCxform());
1356     }
1357     if (tag->hasMatrix()) {
1358         ch->setMatrix(tag->getMatrix(), true);
1359     }
1360 
1361     // use SWFMatrix from the old DisplayObject if tag doesn't provide one.
1362     dlist.replaceDisplayObject(ch, tag->getDepth(),
1363         !tag->hasCxform(), !tag->hasMatrix());
1364     ch->construct();
1365 }
1366 
1367 void
remove_display_object(const SWF::PlaceObject2Tag * tag,DisplayList & dlist)1368 MovieClip::remove_display_object(const SWF::PlaceObject2Tag* tag,
1369         DisplayList& dlist)
1370 {
1371     set_invalidated();
1372     dlist.removeDisplayObject(tag->getDepth());
1373 }
1374 
1375 void
remove_display_object(int depth,int)1376 MovieClip::remove_display_object(int depth, int)
1377 {
1378     set_invalidated();
1379     _displayList.removeDisplayObject(depth);
1380 }
1381 
1382 void
increment_frame_and_check_for_loop()1383 MovieClip::increment_frame_and_check_for_loop()
1384 {
1385     const size_t frame_count = get_loaded_frames();
1386     if (++_currentFrame >= frame_count) {
1387         // Loop.
1388         _currentFrame = 0;
1389         _hasLooped = true;
1390         // Make sure the streaming sound can start again.
1391         stopStreamSound();
1392     }
1393 }
1394 
1395 bool
handleFocus()1396 MovieClip::handleFocus()
1397 {
1398     as_object* obj = getObject(this);
1399     assert(obj);
1400 
1401     // For SWF6 and above: the MovieClip can always receive focus if
1402     // focusEnabled evaluates to true.
1403     if (getSWFVersion(*obj) > 5) {
1404         as_value focusEnabled;
1405         if (obj->get_member(NSV::PROP_FOCUS_ENABLED, &focusEnabled)) {
1406             if (toBool(focusEnabled, getVM(*obj))) return true;
1407         }
1408     }
1409 
1410     // If focusEnabled doesn't evaluate to true or for SWF5, return true
1411     // only if at least one mouse event handler is defined.
1412     return mouseEnabled();
1413 }
1414 
1415 bool
pointInShape(std::int32_t x,std::int32_t y) const1416 MovieClip::pointInShape(std::int32_t x, std::int32_t y) const
1417 {
1418     ShapeContainerFinder finder(x, y);
1419     _displayList.visitBackward(finder);
1420     if ( finder.hitFound() ) return true;
1421     return hitTestDrawable(x, y);
1422 }
1423 
1424 bool
pointInVisibleShape(std::int32_t x,std::int32_t y) const1425 MovieClip::pointInVisibleShape(std::int32_t x, std::int32_t y) const
1426 {
1427     if (! visible()) return false;
1428     if (isDynamicMask() && ! mouseEnabled()) {
1429         // see testsuite/misc-ming.all/masks_test.swf
1430 #ifdef GNASH_DEBUG_HITTEST
1431         log_debug("%s is a dynamic mask and can't handle mouse "
1432                     "events, no point will hit it", getTarget());
1433 #endif
1434         return false;
1435     }
1436     const DisplayObject* mask = getMask(); // dynamic one
1437     if (mask && mask->visible() && !mask->pointInShape(x, y)) {
1438 #ifdef GNASH_DEBUG_HITTEST
1439         log_debug("%s is dynamically masked by %s, which "
1440                 "doesn't hit point %g,%g", getTarget(),
1441                 mask->getTarget(), x, y);
1442 #endif
1443         return false;
1444     }
1445     VisibleShapeContainerFinder finder(x, y);
1446     _displayList.visitBackward(finder);
1447     if (finder.hitFound()) return true;
1448     return hitTestDrawable(x, y);
1449 }
1450 
1451 inline bool
hitTestDrawable(std::int32_t x,std::int32_t y) const1452 MovieClip::hitTestDrawable(std::int32_t x, std::int32_t y) const
1453 {
1454     const SWFMatrix wm = getWorldMatrix(*this).invert();
1455     point lp(x, y);
1456     wm.transform(lp);
1457     if (!_drawable.getBounds().point_test(lp.x, lp.y)) return false;
1458     return _drawable.pointTestLocal(lp.x, lp.y, wm);
1459 }
1460 
1461 bool
pointInHitableShape(std::int32_t x,std::int32_t y) const1462 MovieClip::pointInHitableShape(std::int32_t x, std::int32_t y) const
1463 {
1464     if (isDynamicMask() && !mouseEnabled()) return false;
1465 
1466     const DisplayObject* mask = getMask();
1467     if (mask && !mask->pointInShape(x, y)) return false;
1468 
1469     HitableShapeContainerFinder finder(x, y);
1470     _displayList.visitBackward(finder);
1471     if (finder.hitFound()) return true;
1472 
1473     return hitTestDrawable(x, y);
1474 }
1475 
1476 InteractiveObject*
topmostMouseEntity(std::int32_t x,std::int32_t y)1477 MovieClip::topmostMouseEntity(std::int32_t x, std::int32_t y)
1478 {
1479     if (!visible()) return nullptr;
1480 
1481     // point is in parent's space, we need to convert it in world space
1482     point wp(x, y);
1483     DisplayObject* p = parent();
1484     if (p) {
1485         // WARNING: if we have NO parent, our parent is the Stage (movie_root)
1486         //          so, in case we'll add a "stage" matrix, we'll need to take
1487         //          it into account here.
1488         // TODO: actually, why are we insisting in using parent's
1489         //          coordinates for this method at all ?
1490         getWorldMatrix(*p).transform(wp);
1491     }
1492 
1493     if (mouseEnabled()) {
1494         if (pointInVisibleShape(wp.x, wp.y)) return this;
1495         return nullptr;
1496     }
1497 
1498     SWFMatrix m = getMatrix(*this);
1499     m.invert();
1500     point pp(x, y);
1501     m.transform(pp);
1502 
1503     MouseEntityFinder finder(wp, pp);
1504     _displayList.visitAll(finder);
1505     InteractiveObject* ch = finder.getEntity();
1506 
1507     // It doesn't make any sense to query _drawable, as it's
1508     // not an InteractiveObject.
1509     return ch;
1510 }
1511 
1512 const DisplayObject*
findDropTarget(std::int32_t x,std::int32_t y,DisplayObject * dragging) const1513 MovieClip::findDropTarget(std::int32_t x, std::int32_t y,
1514         DisplayObject* dragging) const
1515 {
1516     if (this == dragging) return nullptr; // not here...
1517 
1518     if (!visible()) return nullptr; // isn't me !
1519 
1520     DropTargetFinder finder(x, y, dragging);
1521     _displayList.visitAll(finder);
1522 
1523     // does it hit any child ?
1524     const DisplayObject* ch = finder.getDropChar();
1525     if (ch) {
1526         // TODO: find closest actionscript referenceable container
1527         //             (possibly itself)
1528         return ch;
1529     }
1530 
1531     // does it hit us ?
1532     if (hitTestDrawable(x, y)) return this;
1533 
1534     return nullptr;
1535 }
1536 
1537 bool
trackAsMenu()1538 MovieClip::trackAsMenu()
1539 {
1540     as_object* obj = getObject(this);
1541     assert(obj);
1542 
1543     as_value track;
1544     VM& vm = getVM(*obj);
1545     // TODO: use namedStrings here
1546     return (obj->get_member(getURI(vm, "trackAsMenu"), &track) &&
1547             toBool(track, vm));
1548 }
1549 
1550 bool
mouseEnabled() const1551 MovieClip::mouseEnabled() const
1552 {
1553     if (!isEnabled()) return false;
1554 
1555     // Event handlers that qualify as mouse event handlers.
1556     static const event_id EH[] = {
1557         event_id(event_id::PRESS),
1558         event_id(event_id::RELEASE),
1559         event_id(event_id::RELEASE_OUTSIDE),
1560         event_id(event_id::ROLL_OVER),
1561         event_id(event_id::ROLL_OUT),
1562         event_id(event_id::DRAG_OVER),
1563         event_id(event_id::DRAG_OUT),
1564     };
1565 
1566     const size_t size = arraySize(EH);
1567 
1568     for (size_t i = 0; i < size; ++i) {
1569         const event_id &event = EH[i];
1570 
1571         // Check event handlers
1572         if (hasEventHandler(event_id(event.id()))) {
1573             return true;
1574         }
1575     }
1576     return false;
1577 }
1578 
1579 void
set_background_color(const rgba & color)1580 MovieClip::set_background_color(const rgba& color)
1581 {
1582     stage().set_background_color(color);
1583 }
1584 
1585 void
cleanup_textfield_variables()1586 MovieClip::cleanup_textfield_variables()
1587 {
1588     // nothing to do
1589     if (!_text_variables.get()) return;
1590 
1591     TextFieldIndex& m = *_text_variables;
1592 
1593     for (auto& index : m)
1594     {
1595         TextFields& v=index.second;
1596         TextFields::iterator lastValid = std::remove_if(v.begin(), v.end(),
1597                     std::mem_fn(&DisplayObject::unloaded));
1598         v.erase(lastValid, v.end());
1599     }
1600 }
1601 
1602 
1603 void
set_textfield_variable(const ObjectURI & name,TextField * ch)1604 MovieClip::set_textfield_variable(const ObjectURI& name, TextField* ch)
1605 {
1606     assert(ch);
1607 
1608     // lazy allocation
1609     if (!_text_variables.get()) {
1610         _text_variables.reset(new TextFieldIndex);
1611     }
1612 
1613     (*_text_variables)[name].push_back(ch);
1614 }
1615 
1616 DisplayObject*
getDisplayListObject(const ObjectURI & uri)1617 MovieClip::getDisplayListObject(const ObjectURI& uri)
1618 {
1619     as_object* obj = getObject(this);
1620     assert(obj);
1621 
1622     string_table& st = getStringTable(*obj);
1623 
1624     // Try items on our display list.
1625     DisplayObject* ch = _displayList.getDisplayObjectByName(st, uri,
1626             caseless(*obj));
1627 
1628     if (!ch) return nullptr;
1629 
1630     // Found object.
1631 
1632     // If the object is an ActionScript referenciable one we
1633     // return it, otherwise we return ourselves
1634     if (isReferenceable(*ch)) {
1635         return ch;
1636     }
1637     return this;
1638 }
1639 
1640 void
add_invalidated_bounds(InvalidatedRanges & ranges,bool force)1641 MovieClip::add_invalidated_bounds(InvalidatedRanges& ranges, bool force)
1642 {
1643     // nothing to do if this movieclip is not visible
1644     if (!visible() || invisible(getCxForm(*this))) {
1645         ranges.add(m_old_invalidated_ranges);
1646         return;
1647     }
1648 
1649     if (!invalidated() && !childInvalidated() && !force) return;
1650 
1651     // m_child_invalidated does not require our own bounds
1652     if (invalidated() || force) {
1653         // Add old invalidated bounds
1654         ranges.add(m_old_invalidated_ranges);
1655     }
1656 
1657     _displayList.add_invalidated_bounds(ranges, force || invalidated());
1658 
1659     /// Add drawable.
1660     SWFRect bounds;
1661     bounds.expand_to_transformed_rect(getWorldMatrix(*this),
1662             _drawable.getBounds());
1663 
1664     ranges.add(bounds.getRange());
1665 }
1666 
1667 
1668 void
constructAsScriptObject()1669 MovieClip::constructAsScriptObject()
1670 {
1671     as_object* mc = getObject(this);
1672 
1673     // A MovieClip should always have an associated object.
1674     assert(mc);
1675 
1676     if (!parent()) {
1677         mc->init_member("$version", getVM(*mc).getPlayerVersion(), 0);
1678     }
1679 
1680     const sprite_definition* def =
1681         dynamic_cast<const sprite_definition*>(_def.get());
1682 
1683     // We won't "construct" top-level movies
1684     as_function* ctor = def ? stage().getRegisteredClass(def) : nullptr;
1685 
1686 #ifdef GNASH_DEBUG
1687     log_debug("Attached movieclips %s registered class is %p",
1688             getTarget(), (void*)ctor);
1689 #endif
1690 
1691     // Set this MovieClip object to be an instance of the class.
1692     if (ctor) {
1693         Property* proto = ctor->getOwnProperty(NSV::PROP_PROTOTYPE);
1694         if (proto) mc->set_prototype(proto->getValue(*ctor));
1695     }
1696 
1697     // Send the construct event. This must be done after the __proto__
1698     // member is set. It is always done.
1699     notifyEvent(event_id(event_id::CONSTRUCT));
1700 
1701     if (ctor) {
1702         const int swfversion = getSWFVersion(*mc);
1703         if (swfversion > 5) {
1704             fn_call::Args args;
1705             ctor->construct(*mc, get_environment(), args);
1706         }
1707     }
1708 }
1709 
1710 void
construct(as_object * initObj)1711 MovieClip::construct(as_object* initObj)
1712 {
1713     assert(!unloaded());
1714 
1715     saveOriginalTarget();
1716 
1717 #ifdef GNASH_DEBUG
1718     log_debug("Sprite '%s' placed on stage", getTarget());
1719 #endif
1720 
1721     // Register this movieclip as a live one
1722     stage().addLiveChar(this);
1723 
1724     // It seems it's legal to place 0-framed movieclips on stage.
1725     // See testsuite/misc-swfmill.all/zeroframe_definemovieclip.swf
1726 
1727     // Now execute frame tags and take care of queuing the LOAD event.
1728     //
1729     // DLIST tags are executed immediately while ACTION tags are queued.
1730     //
1731     // For clips w/out event handlers, LOAD event is invoked *after*
1732     // actions in first frame
1733     // See misc-ming.all/action_order/action_execution_order_test4.{c,swf}
1734     //
1735     assert(!_callingFrameActions); // or will not be queuing actions
1736 
1737     if ( ! get_event_handlers().empty() ) {
1738         queueLoad();
1739     }
1740 
1741     executeFrameTags(0, _displayList, SWF::ControlTag::TAG_DLIST |
1742             SWF::ControlTag::TAG_ACTION);
1743 
1744     // We execute events immediately when the stage-placed DisplayObject
1745     // is dynamic, This is becase we assume that this means that
1746     // the DisplayObject is placed during processing of actions (opposed
1747     // that during advancement iteration).
1748     //
1749     // A more general implementation might ask movie_root about its state
1750     // (iterating or processing actions?)
1751     // Another possibility to inspect could be letting movie_root decide
1752     // when to really queue and when rather to execute immediately the
1753     // events with priority INITIALIZE or CONSTRUCT ...
1754     if (!isDynamic()) {
1755 
1756 #ifdef GNASH_DEBUG
1757         log_debug("Queuing INITIALIZE and CONSTRUCT events for movieclip %s",
1758                 getTarget());
1759 #endif
1760 
1761         std::unique_ptr<ExecutableCode> code(new ConstructEvent(this));
1762         stage().pushAction(std::move(code), movie_root::PRIORITY_CONSTRUCT);
1763 
1764     }
1765     else {
1766 
1767         // Properties from an initObj must be copied before construction, but
1768         // after the display list has been populated, so that _height and
1769         // _width (which depend on bounds) are correct.
1770         if (initObj) {
1771 
1772             as_object* mc = getObject(this);
1773 
1774             // A MovieClip should always have an associated object.
1775             assert(mc);
1776 
1777             mc->copyProperties(*initObj);
1778         }
1779         constructAsScriptObject();
1780     }
1781 
1782     // Tested in testsuite/swfdec/duplicateMovieclip-events.c and
1783     // testsuite/swfdec/clone-sprite-events.c not to call notifyEvent
1784     // immediately.
1785     queueEvent(event_id(event_id::INITIALIZE), movie_root::PRIORITY_INIT);
1786 }
1787 
1788 bool
unloadChildren()1789 MovieClip::unloadChildren()
1790 {
1791 #ifdef GNASH_DEBUG
1792     log_debug("Unloading movieclip '%s'", getTargetPath());
1793 #endif
1794 
1795     // stop any pending streaming sounds
1796     stopStreamSound();
1797 
1798     // We won't be displayed again, so worth releasing
1799     // some memory. The drawable might take a lot of memory
1800     // on itself.
1801     _drawable.clear();
1802 
1803     const bool childHandler = _displayList.unload();
1804 
1805     if (!unloaded()) {
1806         queueEvent(event_id(event_id::UNLOAD), movie_root::PRIORITY_DOACTION);
1807     }
1808 
1809     // Check whether our child MovieClips or this MovieCLip have an unload
1810     // handler.
1811     const bool unloadHandler =
1812         childHandler || hasEventHandler(event_id(event_id::UNLOAD));
1813 
1814     // If there's no unload handler, make sure any queued constructor for
1815     // this MovieClip is not executed!
1816     if (!unloadHandler) {
1817         stage().removeQueuedConstructor(this);
1818     }
1819 
1820     return unloadHandler;
1821 }
1822 
1823 void
getLoadedMovie(Movie * extern_movie)1824 MovieClip::getLoadedMovie(Movie* extern_movie)
1825 {
1826     DisplayObject* p = parent();
1827     if (p) {
1828         extern_movie->set_parent(p);
1829 
1830         // Copy own lockroot value
1831         extern_movie->setLockRoot(getLockRoot());
1832 
1833         // Copy own event handlers
1834         // see testsuite/misc-ming.all/loadMovieTest.swf
1835         const Events& clipEvs = get_event_handlers();
1836         // top-level movies can't have clip events, right ?
1837         assert (extern_movie->get_event_handlers().empty());
1838         extern_movie->set_event_handlers(clipEvs);
1839 
1840         // Copy own name
1841         // TODO: check empty != none...
1842         const ObjectURI& name = get_name();
1843         if (!name.empty()) extern_movie->set_name(name);
1844 
1845         // Copy own clip depth (TODO: check this)
1846         extern_movie->set_clip_depth(get_clip_depth());
1847 
1848         // Replace ourselves in parent
1849         // TODO: don't pretend our parent is a MovieClip,
1850         //       could as well be a button I guess...
1851         //       At most we should require it to be a
1852         //       DisplayObjectContainer and log an error if it's not.
1853         MovieClip* parent_sp = p->to_movie();
1854         assert(parent_sp);
1855         parent_sp->_displayList.replaceDisplayObject(extern_movie, get_depth(),
1856                 true, true);
1857         extern_movie->construct();
1858     }
1859     else {
1860         // replaceLevel will set depth for us
1861         stage().replaceLevel(get_depth() - DisplayObject::staticDepthOffset,
1862                               extern_movie);
1863     }
1864 }
1865 
1866 void
loadVariables(const std::string & urlstr,VariablesMethod sendVarsMethod)1867 MovieClip::loadVariables(const std::string& urlstr,
1868         VariablesMethod sendVarsMethod)
1869 {
1870     // Host security check will be will be done by LoadVariablesThread
1871     // (down by getStream, that is)
1872     const movie_root& mr = stage();
1873     URL url(urlstr, mr.runResources().streamProvider().baseURL());
1874 
1875     std::string postdata;
1876 
1877     // Encode our vars for sending.
1878     if (sendVarsMethod != METHOD_NONE) {
1879         postdata = getURLEncodedVars(*getObject(this));
1880     }
1881 
1882     try {
1883         const StreamProvider& sp =
1884             getRunResources(*getObject(this)).streamProvider();
1885 
1886         if (sendVarsMethod == METHOD_POST) {
1887             // use POST method
1888             _loadVariableRequests.push_back(
1889                     new LoadVariablesThread(sp, url, postdata));
1890         }
1891         else {
1892             // use GET method
1893             if (sendVarsMethod == METHOD_GET) {
1894                 // Append variables
1895                 std::string qs = url.querystring();
1896                 if (qs.empty()) url.set_querystring(postdata);
1897                 else url.set_querystring(qs + "&" + postdata);
1898             }
1899             _loadVariableRequests.push_back(new LoadVariablesThread(sp, url));
1900         }
1901     }
1902     catch (const NetworkException& ex) {
1903         log_error(_("Could not load variables from %s"), url.str());
1904     }
1905 }
1906 
1907 void
processCompletedLoadVariableRequest(LoadVariablesThread & request)1908 MovieClip::processCompletedLoadVariableRequest(LoadVariablesThread& request)
1909 {
1910     MovieVariables vals = request.getValues();
1911     setVariables(vals);
1912 
1913     // We want to call a clip-event too if available, see bug #22116
1914     notifyEvent(event_id(event_id::DATA));
1915 }
1916 
1917 void
processCompletedLoadVariableRequests()1918 MovieClip::processCompletedLoadVariableRequests()
1919 {
1920     // Nothing to do (just for clarity)
1921     if (_loadVariableRequests.empty()) return;
1922 
1923     for (LoadVariablesThreads::iterator it=_loadVariableRequests.begin();
1924             it != _loadVariableRequests.end();) {
1925 
1926         LoadVariablesThread& request = *it;
1927         if (request.completed()) {
1928             processCompletedLoadVariableRequest(request);
1929             it = _loadVariableRequests.erase(it);
1930         }
1931         else ++it;
1932     }
1933 }
1934 
1935 void
setVariables(const MovieVariables & vars)1936 MovieClip::setVariables(const MovieVariables& vars)
1937 {
1938     VM& vm = getVM(*getObject(this));
1939     for (const auto& var : vars) {
1940 
1941         const std::string& name = var.first;
1942         const std::string& val = var.second;
1943         getObject(this)->set_member(getURI(vm, name), val);
1944     }
1945 }
1946 
1947 void
removeMovieClip()1948 MovieClip::removeMovieClip()
1949 {
1950     const int depth = get_depth();
1951     if (depth < 0 || depth > 1048575) {
1952         IF_VERBOSE_ASCODING_ERRORS(
1953             log_aserror(_("removeMovieClip(%s): movieclip depth (%d) out of "
1954                 "the 'dynamic' zone [0..1048575], won't remove"),
1955                 getTarget(), depth);
1956         );
1957         return;
1958     }
1959 
1960     MovieClip* p = dynamic_cast<MovieClip*>(parent());
1961     if (p) {
1962         // second argument is arbitrary, see comments above
1963         // the function declaration in MovieClip.h
1964         p->remove_display_object(depth, 0);
1965     }
1966     else {
1967         // removing _level#
1968         stage().dropLevel(depth);
1969         // I guess this can only happen if someone uses
1970         // _swf.swapDepth([0..1048575])
1971     }
1972 
1973 }
1974 
1975 SWFRect
getBounds() const1976 MovieClip::getBounds() const
1977 {
1978     SWFRect bounds;
1979     BoundsFinder f(bounds);
1980     _displayList.visitAll(f);
1981     SWFRect drawableBounds = _drawable.getBounds();
1982     bounds.expand_to_rect(drawableBounds);
1983 
1984     return bounds;
1985 }
1986 
1987 bool
isEnabled() const1988 MovieClip::isEnabled() const
1989 {
1990     as_object* obj = getObject(this);
1991     assert(obj);
1992 
1993     as_value enabled;
1994     if (!obj->get_member(NSV::PROP_ENABLED, &enabled)) {
1995          // We're enabled if there's no 'enabled' member...
1996          return true;
1997     }
1998     return toBool(enabled, getVM(*obj));
1999 }
2000 
2001 
2002 void
visitNonProperties(KeyVisitor & v) const2003 MovieClip::visitNonProperties(KeyVisitor& v) const
2004 {
2005     DisplayListVisitor dv(v);
2006     _displayList.visitAll(dv);
2007 }
2008 
2009 void
cleanupDisplayList()2010 MovieClip::cleanupDisplayList()
2011 {
2012     _displayList.removeUnloaded();
2013     cleanup_textfield_variables();
2014 }
2015 
2016 void
markOwnResources() const2017 MovieClip::markOwnResources() const
2018 {
2019     ReachableMarker marker;
2020 
2021     _displayList.visitAll(marker);
2022 
2023     _environment.markReachableResources();
2024 
2025     // Mark textfields in the TextFieldIndex
2026     if (_text_variables.get()) {
2027         for (TextFieldIndex::const_iterator i=_text_variables->begin(),
2028                     e=_text_variables->end(); i!=e; ++i) {
2029 
2030             const TextFields& tfs=i->second;
2031             std::for_each(tfs.begin(), tfs.end(),
2032                         std::mem_fn(&DisplayObject::setReachable));
2033         }
2034     }
2035 
2036     // Mark our relative root
2037     _swf->setReachable();
2038 }
2039 
2040 void
destroy()2041 MovieClip::destroy()
2042 {
2043     stopStreamSound();
2044     _displayList.destroy();
2045     DisplayObject::destroy();
2046 }
2047 
2048 Movie*
get_root() const2049 MovieClip::get_root() const
2050 {
2051     return _swf;
2052 }
2053 
2054 MovieClip*
getAsRoot()2055 MovieClip::getAsRoot()
2056 {
2057 
2058     // TODO1: as an optimization, if swf version < 7
2059     //                we might as well just return _swf,
2060     //                the whole chain from this movieclip to it's
2061     //                _swf should have the same version...
2062     DisplayObject* p = parent();
2063 
2064     // no parent, we're the root
2065     if (!p) return this;
2066 
2067     // If we have a parent, we descend to it unless
2068     // our _lockroot is true AND our or the VM's
2069     // SWF version is > 6
2070     const int topSWFVersion = stage().getRootMovie().version();
2071 
2072     if (getDefinitionVersion() > 6 || topSWFVersion > 6) {
2073         if (getLockRoot()) return this;
2074     }
2075 
2076     return p->getAsRoot();
2077 }
2078 
2079 
2080 void
setStreamSoundId(int id)2081 MovieClip::setStreamSoundId(int id)
2082 {
2083     if (id != m_sound_stream_id) {
2084         log_debug("Stream sound id from %d to %d, stopping old",
2085                 m_sound_stream_id, id);
2086         stopStreamSound();
2087     }
2088     m_sound_stream_id = id;
2089 }
2090 
2091 void
stopStreamSound()2092 MovieClip::stopStreamSound()
2093 {
2094     if (m_sound_stream_id == -1) return; // nothing to do
2095 
2096     sound::sound_handler* handler = stage().runResources().soundHandler();
2097 
2098     if (handler) {
2099         handler->stopStreamingSound(m_sound_stream_id);
2100     }
2101 
2102     stage().stopStream(m_sound_stream_id);
2103 
2104     m_sound_stream_id = -1;
2105 }
2106 
2107 void
setPlayState(PlayState s)2108 MovieClip::setPlayState(PlayState s)
2109 {
2110     if (s == _playState) return; // nothing to do
2111     if (s == PLAYSTATE_STOP) stopStreamSound();
2112     _playState = s;
2113 }
2114 
2115 namespace {
2116 
2117 MovieClip::TextFields*
textfieldVar(MovieClip::TextFieldIndex * t,const ObjectURI & name)2118 textfieldVar(MovieClip::TextFieldIndex* t, const ObjectURI& name)
2119 {
2120     // nothing allocated yet...
2121     if (!t) return nullptr;
2122 
2123     // TODO: should variable name be considered case-insensitive ?
2124     MovieClip::TextFieldIndex::iterator it = t->find(name);
2125     if (it == t->end()) return nullptr;
2126     return &(it->second);
2127 }
2128 
2129 } // unnamed namespace
2130 } // namespace gnash
2131