diff --git a/LEGO1/mxdsmediaaction.h b/LEGO1/mxdsmediaaction.h
index 6cddd8c1..72fcf036 100644
--- a/LEGO1/mxdsmediaaction.h
+++ b/LEGO1/mxdsmediaaction.h
@@ -33,6 +33,7 @@ public:
 
 	void CopyMediaSrcPath(const char* p_mediaSrcPath);
 
+	inline MxS32 GetFramesPerSecond() const { return this->m_framesPerSecond; }
 	inline MxS32 GetMediaFormat() const { return this->m_mediaFormat; }
 	inline MxS32 GetPaletteManagement() const { return this->m_paletteManagement; }
 	inline MxLong GetSustainTime() const { return this->m_sustainTime; }
diff --git a/LEGO1/mxlist.h b/LEGO1/mxlist.h
index afdeadf4..11afe695 100644
--- a/LEGO1/mxlist.h
+++ b/LEGO1/mxlist.h
@@ -92,6 +92,8 @@ public:
 	void Destroy();
 	MxBool Next(T& p_obj);
 	MxBool Current(T& p_obj);
+	MxBool First(T& p_obj);
+	MxBool Last(T& p_obj);
 	MxBool Advance();
 	MxBool HasMatch() { return m_match != NULL; }
 	void SetValue(T p_obj);
@@ -229,6 +231,26 @@ inline MxBool MxListCursor<T>::Current(T& p_obj)
 	return m_match != NULL;
 }
 
+template <class T>
+inline MxBool MxListCursor<T>::First(T& p_obj)
+{
+	m_match = m_list->m_first;
+	if (m_match)
+		p_obj = m_match->GetValue();
+
+	return m_match != NULL;
+}
+
+template <class T>
+inline MxBool MxListCursor<T>::Last(T& p_obj)
+{
+	m_match = m_list->m_last;
+	if (m_match)
+		p_obj = m_match->GetValue();
+
+	return m_match != NULL;
+}
+
 template <class T>
 inline MxBool MxListCursor<T>::Advance()
 {
diff --git a/LEGO1/mxloopingsmkpresenter.cpp b/LEGO1/mxloopingsmkpresenter.cpp
index 26cf3d43..7f9a5851 100644
--- a/LEGO1/mxloopingsmkpresenter.cpp
+++ b/LEGO1/mxloopingsmkpresenter.cpp
@@ -1,6 +1,7 @@
 #include "mxloopingsmkpresenter.h"
 
-#include "decomp.h"
+#include "mxautolocker.h"
+#include "mxdsmediaaction.h"
 
 DECOMP_SIZE_ASSERT(MxLoopingSmkPresenter, 0x724);
 
@@ -19,12 +20,112 @@ MxLoopingSmkPresenter::~MxLoopingSmkPresenter()
 // FUNCTION: LEGO1 0x100b49b0
 void MxLoopingSmkPresenter::Init()
 {
-	this->m_unk0x720 = 0;
+	this->m_elapsedDuration = 0;
 	this->m_flags &= 0xfd;
 	this->m_flags &= 0xfb;
 }
 
-// STUB: LEGO1 0x100b49d0
+// FUNCTION: LEGO1 0x100b49d0
 void MxLoopingSmkPresenter::Destroy(MxBool p_fromDestructor)
 {
+	m_criticalSection.Enter();
+	Init();
+	m_criticalSection.Leave();
+
+	if (!p_fromDestructor)
+		MxSmkPresenter::Destroy();
+}
+
+// FUNCTION: LEGO1 0x100b4a00
+void MxLoopingSmkPresenter::VTable0x88()
+{
+	if (m_mxSmack.m_smackTag.Frames == m_currentFrame) {
+		m_currentFrame = 0;
+		// TODO: struct incorrect, Palette at wrong offset?
+		memset(&m_mxSmack.m_smackTag.Palette[4], 0, sizeof(m_mxSmack.m_smackTag.Palette));
+	}
+}
+
+// FUNCTION: LEGO1 0x100b4a30
+void MxLoopingSmkPresenter::NextFrame()
+{
+	MxStreamChunk* chunk = NextChunk();
+
+	if (chunk->GetFlags() & MxStreamChunk::Flag_Bit2) {
+		m_previousTickleStates |= 1 << (unsigned char) m_currentTickleState;
+		m_currentTickleState = TickleState_Repeating;
+	}
+	else {
+		LoadFrame(chunk);
+		AppendChunk(chunk);
+		m_elapsedDuration += 1000 / ((MxDSMediaAction*) m_action)->GetFramesPerSecond();
+	}
+
+	m_subscriber->FUN_100b8390(chunk);
+}
+
+// FUNCTION: LEGO1 0x100b4a90
+void MxLoopingSmkPresenter::VTable0x8c()
+{
+	if (m_action->GetDuration() < m_elapsedDuration) {
+		m_previousTickleStates |= 1 << (unsigned char) m_currentTickleState;
+		m_currentTickleState = TickleState_unk5;
+	}
+	else {
+		MxStreamChunk* chunk;
+		m_cursor->Current(chunk);
+		LoadFrame(chunk);
+		m_elapsedDuration += 1000 / ((MxDSMediaAction*) m_action)->GetFramesPerSecond();
+	}
+}
+
+// FUNCTION: LEGO1 0x100b4b00
+void MxLoopingSmkPresenter::RepeatingTickle()
+{
+	for (MxS16 i = 0; i < m_unk0x5c; i++) {
+		if (!m_cursor->HasMatch()) {
+			MxStreamChunk* chunk;
+			MxStreamChunkListCursor cursor(m_chunks);
+
+			cursor.Last(chunk);
+			MxLong time = chunk->GetTime();
+
+			cursor.First(chunk);
+
+			time -= chunk->GetTime();
+			time += 1000 / ((MxDSMediaAction*) m_action)->GetFramesPerSecond();
+
+			cursor.Reset();
+			while (cursor.Next(chunk))
+				chunk->SetTime(chunk->GetTime() + time);
+
+			m_cursor->Advance();
+		}
+
+		MxStreamChunk* chunk;
+		m_cursor->Current(chunk);
+
+		if (m_action->GetElapsedTime() < chunk->GetTime())
+			break;
+
+		VTable0x8c();
+
+		m_cursor->Next(chunk);
+
+		if (m_currentTickleState != TickleState_Repeating)
+			break;
+	}
+}
+
+// FUNCTION: LEGO1 0x100b4cd0
+MxResult MxLoopingSmkPresenter::AddToManager()
+{
+	MxAutoLocker lock(&m_criticalSection);
+	return MxSmkPresenter::AddToManager();
+}
+
+// FUNCTION: LEGO1 0x100b4d40
+void MxLoopingSmkPresenter::Destroy()
+{
+	Destroy(FALSE);
 }
diff --git a/LEGO1/mxloopingsmkpresenter.h b/LEGO1/mxloopingsmkpresenter.h
index b2eba1ca..2f20676e 100644
--- a/LEGO1/mxloopingsmkpresenter.h
+++ b/LEGO1/mxloopingsmkpresenter.h
@@ -18,11 +18,21 @@ public:
 		return "MxLoopingSmkPresenter";
 	}
 
+	virtual void RepeatingTickle() override;  // vtable+0x24
+	virtual MxResult AddToManager() override; // vtable+0x34
+	virtual void Destroy() override;          // vtable+0x38
+	virtual void NextFrame() override;        // vtable+0x64
+	virtual void VTable0x88() override;       // vtable+0x88
+	virtual void VTable0x8c();                // vtable+0x8c
+
 private:
 	void Init();
 	void Destroy(MxBool p_fromDestructor);
 
-	undefined4 m_unk0x720;
+	MxLong m_elapsedDuration; // 0x720
 };
 
+// SYNTHETIC: LEGO1 0x100b4930
+// MxLoopingSmkPresenter::`scalar deleting destructor'
+
 #endif // MXLOOPINGSMKPRESENTER_H
diff --git a/LEGO1/mxsmkpresenter.cpp b/LEGO1/mxsmkpresenter.cpp
index 442e59a7..709151c3 100644
--- a/LEGO1/mxsmkpresenter.cpp
+++ b/LEGO1/mxsmkpresenter.cpp
@@ -21,7 +21,7 @@ MxSmkPresenter::~MxSmkPresenter()
 // FUNCTION: LEGO1 0x100b38d0
 void MxSmkPresenter::Init()
 {
-	m_unk0x71c = 0;
+	m_currentFrame = 0;
 	memset(&m_mxSmack, 0, sizeof(m_mxSmack));
 	m_flags &= 0xfd;
 	m_flags &= 0xfb;
@@ -65,8 +65,8 @@ void MxSmkPresenter::LoadFrame(MxStreamChunk* p_chunk)
 	MxU8* bitmapData = m_bitmap->GetBitmapData();
 	MxU8* chunkData = p_chunk->GetData();
 
-	MxBool paletteChanged = m_mxSmack.m_frameTypes[m_unk0x71c] & 1;
-	m_unk0x71c++;
+	MxBool paletteChanged = m_mxSmack.m_frameTypes[m_currentFrame] & 1;
+	m_currentFrame++;
 	VTable0x88();
 
 	MxRectList list(TRUE);
@@ -90,13 +90,13 @@ void MxSmkPresenter::LoadFrame(MxStreamChunk* p_chunk)
 void MxSmkPresenter::VTable0x88()
 {
 	if ((m_mxSmack.m_smackTag.SmackerType & 1) != 0) {
-		MxU32 und = (m_unk0x71c % m_mxSmack.m_smackTag.Frames);
-		if (1 < m_unk0x71c && und == 1)
-			m_unk0x71c = 1;
+		MxU32 und = (m_currentFrame % m_mxSmack.m_smackTag.Frames);
+		if (1 < m_currentFrame && und == 1)
+			m_currentFrame = 1;
 	}
 	else {
-		if (m_mxSmack.m_smackTag.Frames == m_unk0x71c) {
-			m_unk0x71c = 0;
+		if (m_mxSmack.m_smackTag.Frames == m_currentFrame) {
+			m_currentFrame = 0;
 			// TODO: struct incorrect, Palette at wrong offset?
 			memset(&m_mxSmack.m_smackTag.Palette[4], 0, sizeof(m_mxSmack.m_smackTag.Palette));
 		}
@@ -111,6 +111,12 @@ void MxSmkPresenter::RealizePalette()
 	delete palette;
 }
 
+// FUNCTION: LEGO1 0x100b42f0
+MxResult MxSmkPresenter::AddToManager()
+{
+	return MxVideoPresenter::AddToManager();
+}
+
 // FUNCTION: LEGO1 0x100b4300
 void MxSmkPresenter::Destroy()
 {
diff --git a/LEGO1/mxsmkpresenter.h b/LEGO1/mxsmkpresenter.h
index c70894d7..95b6a201 100644
--- a/LEGO1/mxsmkpresenter.h
+++ b/LEGO1/mxsmkpresenter.h
@@ -25,6 +25,7 @@ public:
 		return !strcmp(p_name, MxSmkPresenter::ClassName()) || MxVideoPresenter::IsA(p_name);
 	}
 
+	virtual MxResult AddToManager() override;                 // vtable+0x34
 	virtual void Destroy() override;                          // vtable+0x38
 	virtual void LoadHeader(MxStreamChunk* p_chunk) override; // vtable+0x5c
 	virtual void CreateBitmap() override;                     // vtable+0x60
@@ -36,8 +37,9 @@ private:
 	void Init();
 	void Destroy(MxBool p_fromDestructor);
 
-	MxSmack m_mxSmack;     // 0x64
-	undefined4 m_unk0x71c; // 0x71c
+protected:
+	MxSmack m_mxSmack;    // 0x64
+	MxU32 m_currentFrame; // 0x71c
 };
 
 #endif // MXSMKPRESENTER_H