A second cut at TDDing the music players. Simple isn’t easy.

In my quest to learn better and easier ways to use TDD on things, I plan to work further on the spike I started yesterday. My intention is to simplify the music players and get them to a point where I unquestionably prefer them to thee hand-crafted untested one that’s in there now.

I plan to make them simpler, where possible, and to that end, I’ve come up with what I think are simpler requirements.

MusicPlayer
Given a playlist table of name=asset, plays an asset by name, looped, and can return the name of the asset now playing. No default tune is played. If needed in testing, can accept an alternative ‘music’ function. If told to play an asset that is now playing, does not restart the asset, just lets it continue.
SmoothPlayer
Given a playlist table, and a delay in seconds, initializes a MusicPlayer. Can return the name of the asset now playing, via MusicPlayer. No default tune is played. Ensures that anything played plays for at least delay seconds. If need be, delays asking for a new tune until delay has elapsed, then asks.
MonsterPlayer
Creates a playlist for “dungeon” and “battle” music, initializes a SmoothPlayer with a delay of 3 seconds. Checks frequently to see if monsters are near player and if so, switches music to “battle”, otherwise switches it to “dungeon”.

I think that covers it. If new requirements come to light, I’ll edit the above with additions in italic. Let’s get to it.

MusicPlayer

We’ll start with the tests that exist, trim them and tune them, then ensure that the objects are suitable. That is, I’m not planning to start over on the existing objects from yesterday, only to refine them.

Here are the tests:

        _:test("Initial song is default", function()
            local mp = MusicPlayer(nil,fakeMusic)
            _:expect(mp:nowPlaying()).is("default")
            _:expect(tunePlaying).is(asset.downloaded.A_Hero_s_Quest.Dungeon)
        end)
        
        _:test("Song is battle when requested", function()
            local mp = MusicPlayer(nil,fakeMusic)
            mp:play("battle")
            _:expect(mp:nowPlaying()).is("battle")
            _:expect(tunePlaying).is(asset.downloaded.A_Hero_s_Quest.Battle)
        end)
        
        _:test("Song goes back to dungeon when requested", function()
            local mp = MusicPlayer(nil,fakeMusic)
            mp:play("battle")
            mp:play("dungeon")
            _:expect(mp:nowPlaying()).is("dungeon")
            _:expect(tunePlaying).is(asset.downloaded.A_Hero_s_Quest.Dungeon)
        end)
        
        _:test("Player plays nothing if tune is unknown", function()
            local mp = MusicPlayer(nil,fakeMusic)
            tunePlaying = "never set"
            mp:play("unknown")
            _:expect(tunePlaying).is("never set")
        end)
        
        _:test("Player accepts alternative playlist", function()
            local playList = {a=asset.downloaded.A_Hero_s_Quest.Arrow_Shoot_1, b=asset.downloaded.A_Hero_s_Quest.Arrow_Shoot_2, default=asset.downloaded.A_Hero_s_Quest.Curse }
            local mp = MusicPlayer(playList, fakeMusic)
            _:expect(tunePlaying).is(asset.downloaded.A_Hero_s_Quest.Curse)
        end)

We’re not creating our music list by default, so we’ll need one in the tests. And we don’t want a default tune any more.

local MonsterPlayList = {battle=asset.downloaded.A_Hero_s_Quest.Battle, dungeon=asset.downloaded.A_Hero_s_Quest.Dungeon}

I replace the first test with this:

        _:test("Player does not start until requested", function()
            local mp = MusicPlayer(MonsterPlaylist,fakeMusic)
            _:expect(mp:nowPlaying()).is(nil)
        end)

I expect this to fail and drive out some behavior.

1: Player does not start until requested -- Tests:133: attempt to index a nil value (field 'tunes')

I’d better show MusicPlayer before it gets changed, so you can see what I’m doing.

MusicPlayer = class()

function MusicPlayer:init(playList, musicFunction)
    self.music = musicFunction or music
    self.tunes = playList or {}
end

function MusicPlayer:nowPlaying()
    return self.playing
end

function MusicPlayer:play(tuneName)
    local tune = self:tuneFor(tuneName)
    if tune then
        self.playing = tuneName
        self.music(tune, true)
    end
end

function MusicPlayer:tuneFor(tuneName)
    return self.tunes[tuneName]
end

In the above, I’ve already changed the init to make the first test pass. Many others are failing, mostly for not initializing the play list. I’ll change them one at a time, I guess, though I am tempted to blast through and do them all. More thinking, less blasting, might be the best idea.

2: Song is battle when requested  -- Actual: nil, Expected: battle

Test needs the playlist. Still fails same way. I am surprise.

I have typed MonsterPlayList when I meant MonsterPlaylist. And the local needs to be at the top of the tab or the tests don’t know it and they get nil.

Test 2 works. And:

3: Song goes back to dungeon when requested  -- Actual: nil, Expected: dungeon

Needs to be passed the playlist. Works. Test 4 also works:

        _:test("Player plays nothing if tune is unknown", function()
            local mp = MusicPlayer(MonsterPlaylist,fakeMusic)
            tunePlaying = "never set"
            mp:play("unknown")
            _:expect(tunePlaying).is("never set")
        end)

The fifth test is this:

        _:test("Player accepts alternative playlist", function()
            local playList = {a=asset.downloaded.A_Hero_s_Quest.Arrow_Shoot_1, b=asset.downloaded.A_Hero_s_Quest.Arrow_Shoot_2, default=asset.downloaded.A_Hero_s_Quest.Curse }
            local mp = MusicPlayer(playList, fakeMusic)
            _:expect(tunePlaying).is(asset.downloaded.A_Hero_s_Quest.Curse)
        end)

That’s redundant since we now pass in the list and we’re sure that the list works. Delete.

That’s all the tests for MusicPlayer. Let’s review it, and our requirements, and see what we think.

MusicPlayer = class()

function MusicPlayer:init(playList, musicFunction)
    self.music = musicFunction or music
    self.tunes = playList or {}
end

function MusicPlayer:nowPlaying()
    return self.playing
end

function MusicPlayer:play(tuneName)
    local tune = self:tuneFor(tuneName)
    if tune then
        self.playing = tuneName
        self.music(tune, true)
    end
end

function MusicPlayer:tuneFor(tuneName)
    return self.tunes[tuneName]
end

I think that’s pretty tight. We could inline that last method. Want to? OK:

function MusicPlayer:play(tuneName)
    local tune = self.tunes[tuneName]
    if tune then
        self.playing = tuneName
        self.music(tune, true)
    end
end

Better, or worse? One?, or Two? I prefer the method, it expresses my idea better. Putting it back.

First four tests are running. (We deleted the old #5, remember.) Last test is for SmoothPlayer and it fails:

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            local sp = SmoothPlayer(0,nil,fakeMusic)
            sp:play("battle")
            _:expect(sp:nowPlaying()).is("default")
            sp:unlock()
            _:expect(sp:nowPlaying()).is("battle")
        end)

Errors tell me to pass in the playlist:

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            local sp = SmoothPlayer(0,MonsterPlaylist,fakeMusic)
            sp:play("battle")
            _:expect(sp:nowPlaying()).is("default")
            sp:unlock()
            _:expect(sp:nowPlaying()).is("battle")
        end)

Now the test fails looking for default. We need to decide what to do here. Let’s do this test over from scratch rather than try to debug it. We might as well follow the pattern we expect in the game, namely starting with “dungeon” and then going to “battle” and back.

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            local sp = SmoothPlayer(0,MonsterPlaylist,fakeMusic)
            sp:play("dungeon")
            sp:play("battle")
            _:expect(sp:nowPlaying()).is("dungeon")
            sp:unlock()
            _:expect(sp:nowPlaying()).is("battle")
        end)

We have that zero being passed in to SmoothPlayer. That’s the delay, used to turn off the tween. For now we’ll let that be, but I really want to test the tween calls if I can think of a decent way to do it.

Will this test run? Let’s find out. First assert fails:

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: nil, Expected: dungeon

Code is bad. Here it is as it stands:

SmoothPlayer = class()

function SmoothPlayer:init(delay, playList, musicFunction)
    self.delay = delay or 0
    self.mp = MusicPlayer(playList, musicFunction)
    self.onDeck = self.mp:nowPlaying()
    self:setTimer()
end

function SmoothPlayer:nowPlaying()
    return self.mp:nowPlaying()
end

function SmoothPlayer:play(tune)
    self.onDeck = tune
end

function SmoothPlayer:playOnDeckTune()
    if self:nowPlaying() ~= self.onDeck then
        self.mp:play(self.onDeck)
    end
end

function SmoothPlayer:setTimer()
    if self.delay == 0 then return end
    tween.delay(self.delay, self.unlock, self)
end

function SmoothPlayer:unlock()
    self:playOnDeckTune()
    self:setTimer()
end

The init is overly elaborate now. We should set onDeck to nil, as we know from here that MP isn’t playing anything. And there’s no reason to set the timer, which should be set when we play something.

function SmoothPlayer:init(delay, playList, musicFunction)
    self.delay = delay or 0
    self.mp = MusicPlayer(playList, musicFunction)
    self.onDeck = nil
end

Error is still:

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: nil, Expected: dungeon

Our play logic in SmoothPlayer is no long up to spec. Let’s remove all its methods and test-drive it anew.

5: Smooth player doesn't accept new tune until time elapsed -- Tests:47: attempt to call a nil value (method 'play')

Ah, good old TDD. Build a simple play:

function SmoothPlayer:play(aTune)
    self.mp:play(aTune)
end

This is of course wrong, as it is not sensitive to the timer, which we don’t have. We’ll get there.

5: Smooth player doesn't accept new tune until time elapsed -- Tests:49: attempt to call a nil value (method 'nowPlaying')
function SmoothPlayer:nowPlaying()
    return self.mp:nowPlaying()
end

This should fail finding the wrong tune.

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: battle, Expected: dungeon

We accepted the tune immediately. We have to wait for the unlock as the test demands:

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            local sp = SmoothPlayer(0,MonsterPlaylist,fakeMusic)
            sp:play("dungeon")
            sp:play("battle")
            _:expect(sp:nowPlaying()).is("dungeon")
            sp:unlock()
            _:expect(sp:nowPlaying()).is("battle")
        end)

This is somewhat subtle. We could just put our call to play in unlock, but then our first song wouldn’t play. We need to have some kind of “ready” flag that toggles, and we’re going to need to … to what? One thing at a time.

We’ll play if ready:

function SmoothPlayer:init(delay, playList, musicFunction)
    self.delay = delay or 0
    self.mp = MusicPlayer(playList, musicFunction)
    self.onDeck = nil
    self.ready = true
end

function SmoothPlayer:play(aTune)
    if self.ready then
        self.mp:play(aTune)
        self.ready = false
    end
end

This should progress the test.

5: Smooth player doesn't accept new tune until time elapsed -- Tests:50: attempt to call a nil value (method 'unlock')

We need unlock, which is trivial:

function SmoothPlayer:unlock()
    self.ready = true
end

Test should still fail.

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: dungeon, Expected: battle

The time has elapsed (I’m assuming that unlock is called by the tween, by the way) and we have not played the song.

Upon unlock, we want to initiate play if the song isn’t already playing. We’ll need to record what it is, I suppose.

function SmoothPlayer:unlock()
    self.ready = true
    self:playIfNotAlreadyPlaying()
end
function SmoothPlayer:playIfNotAlreadyPlaying()
    if self.onDeck ~= self:nowPlaying() then
        self:play(self.onDeck)
    end
end

Now I’ve not recorded the on deck tune, and I am aware of that, but if I had forgotten the test would tell me by continuing to fail:

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: dungeon, Expected: battle

So:

function SmoothPlayer:play(aTune)
    self.onDeck = aTune
    if self.ready then
        self.mp:play(aTune)
        self.ready = false
    end
end

Now I expect this test to pass. And it does.

However, we have not set the timer. Yesterday, I just figured it out and put it in. And I wanted to be able to test with a delay that could be set to zero, which is reflected in the test we already have. Let’s not do that. Instead, let’s bite the bullet and mock tween. Should we default the delay to zero now? That seems risky. Let’s default it to 3.

Now what about testing the timer? Let’s enhance test 5 to ask questions about the tween, and then try to answer them.

I need to think about this. I need to think so hard that I need a new heading.

TweenTime

When we actually start playing a tune, we want to set a delay tween for delay seconds. When that tween fires, we want it to cal unlock. We do not want it to start again … but if the on deck tune is different from the now-playing tune, the on deck will be played and that will start the tween again.

We could cater for an early call to unlock but let’s not. Let’s just check to see when tween.delay is called and what it’s called with.

I think what I want is this:

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            tweentime = nil
            local sp = SmoothPlayer(4,MonsterPlaylist,fakeMusic)
            _:expect(tweenTime).is(nil)
            sp:play("dungeon")
            _:expect(tweenTime).is(4)
            sp:play("battle")
            _:expect(tweenTime).is(4)
            _:expect(sp:nowPlaying()).is("dungeon")
            sp:unlock()
            _:expect(tweenTime).is(4)
            _:expect(sp:nowPlaying()).is("battle")
            tweenTime = nil
            sp:unlock()
            _:expect(tweenTime).is(nil)
        end)

The player starts with no tween time set. When it plays its first tune, it starts immediately and tween time should be set to 4 because 4 is our number. (I have just changed my mind about the default. Provide whatever number you want, it’s the first parm.)

This code is written to assume that somehow tweentime gets set. My plan is to provide a fake tweendelay function, like the fake music one.

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            tweentime = nil
            local sp = SmoothPlayer(4,MonsterPlaylist,fakeMusic, fakeTweenDelay)
            _:expect(tweenTime).is(nil)
            sp:play("dungeon")
...

function fakeTweenDelay(time)
    tweentime = time
end
5: Smooth player doesn't accept new tune until time elapsed  -- Actual: nil, Expected: 4

No surprise, we’re not setting the timer yet.

function SmoothPlayer:playIfNotAlreadyPlaying()
    if self.onDeck ~= self:nowPlaying() then
        self:play(self.onDeck)
        self.tweenDelay(self.delay, self.unlock, self)
    end
end

I rather expect this to work, unless the test is wrong. Let’s see:

5: Smooth player doesn't accept new tune until time elapsed  -- Actual: nil, Expected: 4

Of course I’m not totally sure which one this is, so let’s enhance the test.

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            tweenTime = 9
            local sp = SmoothPlayer(4,MonsterPlaylist,fakeMusic, fakeTweenDelay)
            _:expect(tweenTime, "at start").is(9)
            sp:play("dungeon")
            _:expect(tweenTime, "after dungeon").is(4)
            sp:play("battle")
            _:expect(tweenTime, "after battle attempt").is(4)
            _:expect(sp:nowPlaying()).is("dungeon")
            tweentime = 99
            sp:unlock()
            _:expect(tweenTime, "after delayed play of battle").is(4)
            _:expect(sp:nowPlaying()).is("battle")
            tweenTime = 999
            sp:unlock()
            _:expect(tweenTime).is(999)
        end)

CodeaUnit lets me put a comment after the is parameter, and displays that as part of any error messages.

I’ve also tried hard to always use tweenTime and not tweentime. I really need to do something about newly created undefined variables. Anyway …

5: Smooth player doesn't accept new tune until time elapsed after dungeon -- Actual: 9, Expected: 4
5: Smooth player doesn't accept new tune until time elapsed after battle attempt -- Actual: 9, Expected: 4

OK, let’s see what’s up here. tweenTime doesn’t turn from 9 until 4 until “after delayed play of battle”. It should get set on the first call, to play “dungeon”. And I should probably consider overwriting the second 4 there. One thing at a time.

Ah. Here’s the relevant code:

function SmoothPlayer:play(aTune)
    self.onDeck = aTune
    if self.ready then
        self.mp:play(aTune)
        self.ready = false
    end
end

function SmoothPlayer:playIfNotAlreadyPlaying()
    if self.onDeck ~= self:nowPlaying() then
        self:play(self.onDeck)
        self.tweenDelay(self.delay, self.unlock, self)
    end
end

We just call in on play with no checking, so we never set the timer. What’s the right sequence here?

We certainly want to save whatever’s requested as onDeck. But then, if we’re not ready, we shouldn’t play. If we are ready, then if it’s not what’s already playing, we should play it and set the timer. Also, where we set the timer is where we should set not ready. So how about this?

function SmoothPlayer:play(aTune)
    self.onDeck = aTune
    if self.ready then
        self:playIfNotAlreadyPlaying()
    end
end

function SmoothPlayer:playIfNotAlreadyPlaying()
    if self.onDeck ~= self:nowPlaying() then
        self:play(self.onDeck)
        self.tweenDelay(self.delay, self.unlock, self)
        self.ready = false
    end
end

Whee!

5: Smooth player doesn't accept new tune until time elapsed -- Tests:101: stack overflow

That’s not good. We had best not call play after play has called us. Should have said:

function SmoothPlayer:playIfNotAlreadyPlaying()
    if self.onDeck ~= self:nowPlaying() then
        self.mp:play(self.onDeck)
        self.tweenDelay(self.delay, self.unlock, self)
        self.ready = false
    end
end

OK, my test now runs. Let’s review it

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            tweenTime = 9
            local sp = SmoothPlayer(4,MonsterPlaylist,fakeMusic, fakeTweenDelay)
            _:expect(tweenTime, "at start").is(9)
            sp:play("dungeon")
            _:expect(tweenTime, "after dungeon").is(4)
            sp:play("battle")
            _:expect(tweenTime, "after battle attempt").is(4)
            _:expect(sp:nowPlaying()).is("dungeon")
            tweentime = 99
            sp:unlock()
            _:expect(tweenTime, "after delayed play of battle").is(4)
            _:expect(sp:nowPlaying()).is("battle")
            tweenTime = 999
            sp:unlock()
            _:expect(tweenTime).is(999)
        end)

We set tweenTime to an impossible value, then start the player, which does not start the timer. We play “dungeon”, which is new, so it sets the timer to 4. We try to play “battle”, which does not play and the timer is still 4. We’re not sure if the delay was called, so let’s clobber the value and do it this way:

        _:test("Smooth player doesn't accept new tune until time elapsed", function()
            tweenTime = 9
            local sp = SmoothPlayer(4,MonsterPlaylist,fakeMusic, fakeTweenDelay)
            _:expect(tweenTime, "at start").is(9)
            sp:play("dungeon")
            _:expect(tweenTime, "after dungeon").is(4)
            tweenTime = 8
            sp:play("battle")
            _:expect(tweenTime, "after battle attempt").is(8)
            _:expect(sp:nowPlaying()).is("dungeon")
            tweentime = 99
            sp:unlock()
            _:expect(tweenTime, "after delayed play of battle").is(4)
            _:expect(sp:nowPlaying()).is("battle")
            tweenTime = 999
            sp:unlock()
            _:expect(tweenTime).is(999)
        end)

That continues to work. So calling for “battle” does not play it, and does not set the timer (which was already set when we played “dungeon”). Now the time runs out (we call unlock directly). We expect that battle will start playing and that tweenTime will again be 4. And it is. When we unlock again, the on deck tune has not changed, so the timer is not set.

That seems right to me.

And we’ve actually tested that we’re getting the right calls to the tween.delay, at least as regards the time set.

Are We There Yet?

What about the MonsterPLayer? It looks like this:

MonsterPlayer = class()

function MonsterPlayer:init(runner)
    self.runner = runner
    self.sp = SmoothPlayer(3)
    tween.delay(1, self.everySoOften, self)
end

function MonsterPlayer:checkForDanger()
    local close = self.runner:hasMonsterNearPlayer(5)
    if close then self.sp:play("battle") else self.sp:play("dungeon") end
end

function MonsterPlayer:everySoOften()
    self:checkForDanger()
    tween.delay(1, self.everySoOften, self)
end

And we have no tests for it, not one, zero, not any. There’s not much there to test. With our new notion that the players don’t know their play list, we need to put that in here.

There’s not enough branching logic here to justify testing even in my present frame of mind. Let’s paste all this stuff over and see what happens.

To plug it in, we change GameRunner:

function GameRunner:init()
    self.tileSize = 64
    self.tileCountX = 85 -- if these change, zoomed-out scale 
    self.tileCountY = 64 -- may also need to be changed.
    self.tiles = {}
    for x = 1,self.tileCountX+1 do
        self.tiles[x] = {}
        for y = 1,self.tileCountY+1 do
            local tile = Tile:edge(x,y, self)
            self:setTile(tile)
        end
    end
    self.cofloater = Floater(self, 50,25,4)
    self.musicPlayer = MonsterPlayer(self)
end

It works nicely, as we see here:

sound

Yesterday’s version waited a bit too long to start the music or switch it. Today’s works just like I had in mind.

This time, I think we’ll keep the new tests and code, and sum up.

Summing Up

The commit says -30+130 lines. Total lines are now 150, of which 76 are test, 74 code. The old MusicPlayer was 46 lines and did everything, untested, in one class. If we need no additional music changes, this exercise will surely not pay off. If there are, I suspect they’ll be easier in this setup. Most likely, MonsterPlayer would have to change or be replaced, but the other two classes are just about guaranteed to stand.

I can easily generate another requirement for music. I think it was Bryan who suggested a happy tune if the princess finds a treasure. Perhaps it was Asa who suggested a different tune at the beginning of the game, or the end. In any case, we could “force” the issue by showing how easily new music requirements can be put up in this bi…tty little program, but right now, we have other fish to fry.

It builds the stack

One thing that I consider iffy about this design is that the stack is all built internally, the MonsterPlayer creates its own SmoothPlayer, which creates its own MusicPlayer. It could be argued that those should be injected somehow, or that there should be factory methods, perhaps like:

function MonsterPlayer:withPlayer(runner, aPlayer)

That would provide the flexibility of plugging in other kinds of players. This might be true, but by me it’s more generality than we need at this time, so, no.

It has extra mechanism

The ability to provide fake functions just for testing is a bit concerning, but it did turn out to be useful. We built a fakeMusic function with enough of the music protocol to let us detect what tune we were playing, and a fakeTweenDelay1 function that let us do the same with tween.delay`.

It might be possible–probably would be possible–to do this by masking the global objects music and tween or tween.delay during tests and then unmasking them later. Or we could provide separate testing-related functions that we could call to inject the fakes.

Finally, it would also be possible to make those functions required parameters instead of optional, and to provide the real ones in our real calls to create the players.

Same-same in my book, and better to allow most users just to pass in the things they care about, the delays and playlists.

Dependencies

Codea allows a project to declare dependencies, references from one project to another. Basically it imports all the tabs from the other project other than Main. So we could, in principle, arrange my separate spike project, MusicPlayer, such that we could just import the player classes. That would save source code and tabs in the main Dung project, but with the disadvantage that changes to the music would probably require changes to that other project first.

For my purposes, I’m happy to copy and paste over something like this, but if I ever decide to use it again, setting up for dependency might be worth it.

My bottom line

At least for now, I’m glad we tried this second pass at the MusicPlayer, because the one we had was intricate enough that it doesn’t quite obviously work, and that’s a sign that a thing probably ought to be tested.

I’m also pleased because I found a way to test this kind of thing a bit more easily, with the injection of fake testing functions that I could use to check necessary callbacks and the like. That’s a thing that test doubles are good at and, for me, using a fake function is a bit lighter weight than using a test double object. We could have done it that way, of course, creating either a FakeMusic and FakeTween object, or a single object with both aspects.

Then we’d have more code on the testing side, and we’d need more code on the working side, likely including at least one new object, since neither music nor tweens are covered by objects, only functions. In my view, not a winner over what we have now.

All in all, I’m satisfied with this exercise, but it’s because of the learning more than the impact on the overall program or project. Yes, the players are arguably better-structured, at the cost of nearly double the code, and better tested, at the cost of tripling the code size if we leave the tests in the dungeon program.

If my manager were to ask the project team why we spent two whole mornings creating tests and better code for something that already worked, my arguments would all be about the future. Learning lets us go faster and now we’re more able to do additional music and now we can be sure that it works, and so on, singing and dancing.

But for me, and for us, sitting right here, I feel the investment was worth it.

But I’d have to say that, would’t I?

See you next time!


D2.zip