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