2006-02-24 04:48:15 +00:00
|
|
|
/*
|
|
|
|
** i_movie.cpp
|
|
|
|
**
|
|
|
|
**---------------------------------------------------------------------------
|
2006-06-11 01:37:00 +00:00
|
|
|
** Copyright 1998-2006 Randy Heit
|
2006-02-24 04:48:15 +00:00
|
|
|
** All rights reserved.
|
|
|
|
**
|
|
|
|
** Redistribution and use in source and binary forms, with or without
|
|
|
|
** modification, are permitted provided that the following conditions
|
|
|
|
** are met:
|
|
|
|
**
|
|
|
|
** 1. Redistributions of source code must retain the above copyright
|
|
|
|
** notice, this list of conditions and the following disclaimer.
|
|
|
|
** 2. Redistributions in binary form must reproduce the above copyright
|
|
|
|
** notice, this list of conditions and the following disclaimer in the
|
|
|
|
** documentation and/or other materials provided with the distribution.
|
|
|
|
** 3. The name of the author may not be used to endorse or promote products
|
|
|
|
** derived from this software without specific prior written permission.
|
|
|
|
**
|
|
|
|
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
|
|
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
|
|
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
|
|
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
|
|
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
|
|
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
|
|
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
|
|
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
|
|
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
**---------------------------------------------------------------------------
|
|
|
|
**
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
* If you do not have dshow.h, either download the latest DirectX SDK from
|
|
|
|
* <http://msdn.microsoft.com/library/default.asp?url=/nhp/Default.asp?contentid=28000410>
|
|
|
|
* or #define I_DO_NOT_LIKE_BIG_DOWNLOADS
|
|
|
|
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "c_cvars.h"
|
|
|
|
|
|
|
|
CUSTOM_CVAR (Float, snd_movievolume, 1.f, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
|
|
|
|
{
|
|
|
|
if (self < 0.f)
|
|
|
|
self = 0.f;
|
|
|
|
else if (self > 1.f)
|
|
|
|
self = 1.f;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef I_DO_NOT_LIKE_BIG_DOWNLOADS
|
|
|
|
|
|
|
|
#include "i_movie.h"
|
|
|
|
|
|
|
|
int I_PlayMovie (const char *movie)
|
|
|
|
{
|
|
|
|
return MOVIE_Failed;
|
|
|
|
}
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
|
|
|
|
|
|
#include <windows.h>
|
|
|
|
#include <dshow.h>
|
|
|
|
|
|
|
|
#include "i_movie.h"
|
|
|
|
#include "i_sound.h"
|
|
|
|
#include "v_video.h"
|
|
|
|
#include "c_console.h"
|
|
|
|
#include "win32iface.h"
|
|
|
|
#include "sbar.h"
|
|
|
|
|
|
|
|
EXTERN_CVAR (String, language)
|
|
|
|
|
|
|
|
#define WM_GRAPHNOTIFY (WM_APP+321)
|
|
|
|
|
|
|
|
#ifdef _DEBUG
|
|
|
|
#define INGAME_PRIORITY_CLASS NORMAL_PRIORITY_CLASS
|
|
|
|
#else
|
|
|
|
//#define INGAME_PRIORITY_CLASS HIGH_PRIORITY_CLASS
|
|
|
|
#define INGAME_PRIORITY_CLASS NORMAL_PRIORITY_CLASS
|
|
|
|
#endif
|
|
|
|
|
|
|
|
extern HWND Window;
|
|
|
|
extern IVideo *Video;
|
|
|
|
extern LRESULT CALLBACK WndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
|
|
|
extern void I_CheckNativeMouse (bool preferNative);
|
|
|
|
|
|
|
|
static bool MovieNotDone;
|
|
|
|
static bool MovieInterrupted;
|
|
|
|
static bool MovieDestroyed;
|
|
|
|
static bool FullVideo;
|
|
|
|
static bool NoVideo;
|
|
|
|
static IVideoWindow *vidwin;
|
|
|
|
static IMediaEventEx *event;
|
|
|
|
static IGraphBuilder *graph;
|
|
|
|
static IMediaControl *control;
|
|
|
|
static IBasicAudio *audio;
|
|
|
|
static IBasicVideo *video;
|
|
|
|
|
|
|
|
static void CheckIfVideo ();
|
|
|
|
static void SetMovieSize ();
|
|
|
|
static void SetTheVolume ();
|
|
|
|
static void SizeWindowForVideo ();
|
|
|
|
|
|
|
|
LRESULT CALLBACK MovieWndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
|
|
{
|
|
|
|
switch (message)
|
|
|
|
{
|
|
|
|
case WM_GRAPHNOTIFY:
|
|
|
|
{
|
|
|
|
long code, parm1, parm2;
|
|
|
|
|
|
|
|
while (event->GetEvent (&code, &parm1, &parm2, 0) == S_OK)
|
|
|
|
{
|
|
|
|
if (code == EC_COMPLETE)
|
|
|
|
{
|
|
|
|
MovieNotDone = false;
|
|
|
|
}
|
|
|
|
event->FreeEventParams (code, parm1, parm2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_SIZE:
|
|
|
|
if (vidwin == NULL)
|
|
|
|
{
|
|
|
|
InvalidateRect (Window, NULL, FALSE);
|
|
|
|
}
|
|
|
|
else if ((wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) && !FullVideo)
|
|
|
|
{
|
|
|
|
SetMovieSize ();
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_PAINT:
|
|
|
|
if (vidwin == NULL)
|
|
|
|
{
|
|
|
|
if (screen != NULL)
|
|
|
|
{
|
|
|
|
static_cast<BaseWinFB *> (screen)->PaintToWindow ();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (!FullVideo)
|
|
|
|
{
|
|
|
|
PAINTSTRUCT ps;
|
|
|
|
HBRUSH br;
|
|
|
|
HDC dc;
|
|
|
|
long left, top, width, height;
|
|
|
|
|
|
|
|
if (S_OK == vidwin->GetWindowPosition (&left, &top, &width, &height))
|
|
|
|
{
|
|
|
|
dc = BeginPaint (Window, &ps);
|
|
|
|
if (dc != NULL)
|
|
|
|
{
|
|
|
|
RECT rect = { left, top, left+width, top+height };
|
|
|
|
ScreenToClient (hWnd, (LPPOINT)&rect.left);
|
|
|
|
ScreenToClient (hWnd, (LPPOINT)&rect.right);
|
|
|
|
br = (HBRUSH)GetStockObject (BLACK_BRUSH);
|
|
|
|
switch (ExcludeClipRect (dc, rect.left, rect.top, rect.right, rect.bottom))
|
|
|
|
{
|
|
|
|
case SIMPLEREGION:
|
|
|
|
case COMPLEXREGION:
|
|
|
|
FillRect (dc, &ps.rcPaint, br);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
EndPaint (Window, &ps);
|
|
|
|
//return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_DESTROY:
|
|
|
|
case WM_KEYDOWN:
|
|
|
|
case WM_SYSKEYDOWN:
|
|
|
|
/*
|
|
|
|
case WM_LBUTTONDOWN:
|
|
|
|
case WM_RBUTTONDOWN:
|
|
|
|
case WM_MBUTTONDOWN:
|
|
|
|
*/
|
|
|
|
if (MovieNotDone)
|
|
|
|
{
|
|
|
|
if (wParam == VK_ESCAPE || message == WM_CANCELMODE || message == WM_DESTROY)
|
|
|
|
{
|
|
|
|
control->Stop ();
|
|
|
|
MovieNotDone = false;
|
|
|
|
MovieInterrupted = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (message == WM_DESTROY)
|
|
|
|
{
|
|
|
|
MovieDestroyed = true;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case WM_GETMINMAXINFO:
|
|
|
|
if (screen && !FullVideo)
|
|
|
|
{
|
|
|
|
LPMINMAXINFO mmi = (LPMINMAXINFO)lParam;
|
|
|
|
mmi->ptMinTrackSize.x = SCREENWIDTH + GetSystemMetrics (SM_CXSIZEFRAME) * 2;
|
|
|
|
mmi->ptMinTrackSize.y = SCREENHEIGHT + GetSystemMetrics (SM_CYSIZEFRAME) * 2 +
|
|
|
|
GetSystemMetrics (SM_CYCAPTION);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_SETTINGCHANGE:
|
|
|
|
// In case regional settings were changed, reget preferred languages
|
|
|
|
language.Callback ();
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return DefWindowProc (hWnd, message, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
int I_PlayMovie (const char *name)
|
|
|
|
{
|
|
|
|
int returnval = MOVIE_Failed;
|
|
|
|
size_t namelen = strlen (name) + 1;
|
|
|
|
wchar_t *uniname = new wchar_t[namelen];
|
|
|
|
bool returnSound = false;
|
|
|
|
bool runningFull = false;
|
|
|
|
bool hotkey = false;
|
|
|
|
size_t i;
|
|
|
|
MSG msg;
|
|
|
|
|
|
|
|
MovieNotDone = true;
|
|
|
|
|
|
|
|
if (MultiByteToWideChar (CP_ACP, MB_PRECOMPOSED, name, -1, uniname, (int)namelen) == 0)
|
|
|
|
{ // Could not convert name to Unicode
|
|
|
|
goto bomb1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert slashes to backslashes because IGraphBuilder cannot deal with them
|
|
|
|
for (i = 0; i < namelen; ++i)
|
|
|
|
{
|
|
|
|
if (uniname[i] == L'/')
|
|
|
|
uniname[i] = L'\\';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (FAILED(CoCreateInstance (CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
|
|
|
|
IID_IGraphBuilder, (void **)&graph)))
|
|
|
|
{
|
|
|
|
goto bomb1;
|
|
|
|
}
|
|
|
|
|
|
|
|
control = NULL;
|
|
|
|
event = NULL;
|
|
|
|
vidwin = NULL;
|
|
|
|
audio = NULL;
|
|
|
|
video = NULL;
|
|
|
|
|
|
|
|
if (FAILED(graph->RenderFile (uniname, NULL)))
|
|
|
|
{
|
|
|
|
goto bomb2;
|
|
|
|
}
|
|
|
|
|
|
|
|
graph->QueryInterface (IID_IMediaControl, (void **)&control);
|
|
|
|
graph->QueryInterface (IID_IMediaEventEx, (void **)&event);
|
|
|
|
graph->QueryInterface (IID_IVideoWindow, (void **)&vidwin);
|
|
|
|
graph->QueryInterface (IID_IBasicAudio, (void **)&audio);
|
|
|
|
graph->QueryInterface (IID_IBasicVideo, (void **)&video);
|
|
|
|
|
|
|
|
if (control == NULL || event == NULL)
|
|
|
|
{
|
|
|
|
goto bomb3;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GSnd != NULL)
|
|
|
|
{
|
|
|
|
GSnd->MovieDisableSound ();
|
|
|
|
}
|
|
|
|
returnSound = true;
|
|
|
|
|
|
|
|
CheckIfVideo ();
|
|
|
|
SetTheVolume ();
|
|
|
|
|
|
|
|
FullVideo = false;
|
|
|
|
|
|
|
|
if (vidwin != NULL)
|
|
|
|
{
|
|
|
|
FullVideo = runningFull = screen->IsFullscreen ();
|
|
|
|
|
|
|
|
// If the message drain cannot be set, then we simply won't be able
|
|
|
|
// to catch mouse button presses in the video window.
|
|
|
|
HRESULT drainhr = vidwin->put_MessageDrain ((OAHWND)Window);
|
|
|
|
|
|
|
|
if (FullVideo)
|
|
|
|
{
|
|
|
|
// Try to avoid nasty palette flashes by clearing the screen to black.
|
|
|
|
// Does not always work. :-(
|
|
|
|
static_cast<Win32Video *> (Video)->BlankForGDI ();
|
|
|
|
static_cast<Win32Video *> (Video)->GoFullscreen (false);
|
|
|
|
static_cast<BaseWinFB *> (screen)->ReleaseResources ();
|
|
|
|
if (FAILED (drainhr) || FAILED(vidwin->put_FullScreenMode (OATRUE)))
|
|
|
|
{
|
|
|
|
SizeWindowForVideo ();
|
|
|
|
FullVideo = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!FullVideo)
|
|
|
|
{
|
|
|
|
vidwin->put_Owner ((OAHWND)Window);
|
|
|
|
vidwin->put_WindowStyle (WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
|
|
|
|
SetMovieSize ();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
RegisterHotKey (Window, 0, MOD_ALT, VK_TAB);
|
|
|
|
hotkey = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (FAILED (event->SetNotifyWindow ((OAHWND)Window, WM_GRAPHNOTIFY, 0)))
|
|
|
|
{
|
|
|
|
goto bomb3;
|
|
|
|
}
|
|
|
|
|
|
|
|
SetPriorityClass (GetCurrentProcess(), NORMAL_PRIORITY_CLASS);
|
|
|
|
|
|
|
|
I_CheckNativeMouse (true);
|
|
|
|
SetWindowLongPtr (Window, GWL_WNDPROC, (LONG_PTR)MovieWndProc);
|
|
|
|
|
|
|
|
if (FAILED (control->Run ()))
|
|
|
|
{
|
|
|
|
goto bomb4;
|
|
|
|
}
|
|
|
|
|
|
|
|
MovieInterrupted = false;
|
|
|
|
MovieDestroyed = false;
|
|
|
|
|
|
|
|
while (MovieNotDone && GetMessage (&msg, NULL, 0, 0))
|
|
|
|
{
|
|
|
|
TranslateMessage (&msg);
|
|
|
|
DispatchMessage (&msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
returnval = MovieInterrupted ? MOVIE_Played_Aborted :
|
|
|
|
NoVideo ? MOVIE_Played_NoVideo :
|
|
|
|
MOVIE_Played;
|
|
|
|
|
|
|
|
bomb4:
|
|
|
|
SetWindowLongPtr (Window, GWL_WNDPROC, (LONG_PTR)WndProc);
|
|
|
|
SetPriorityClass (GetCurrentProcess(), INGAME_PRIORITY_CLASS);
|
|
|
|
|
|
|
|
bomb3:
|
|
|
|
if (hotkey)
|
|
|
|
{
|
|
|
|
UnregisterHotKey (Window, 0);
|
|
|
|
}
|
|
|
|
if (vidwin != NULL)
|
|
|
|
{
|
|
|
|
if (!FullVideo)
|
|
|
|
{
|
|
|
|
vidwin->put_Visible (OAFALSE);
|
|
|
|
vidwin->put_Owner (NULL);
|
|
|
|
}
|
|
|
|
vidwin->Release ();
|
|
|
|
}
|
|
|
|
if (video != NULL)
|
|
|
|
{
|
|
|
|
video->Release ();
|
|
|
|
}
|
|
|
|
if (audio != NULL)
|
|
|
|
{
|
|
|
|
audio->Release ();
|
|
|
|
}
|
|
|
|
if (event != NULL)
|
|
|
|
{
|
|
|
|
event->Release ();
|
|
|
|
}
|
|
|
|
if (control != NULL)
|
|
|
|
{
|
|
|
|
control->Release ();
|
|
|
|
}
|
|
|
|
|
|
|
|
SetActiveWindow (Window);
|
|
|
|
|
|
|
|
bomb2:
|
|
|
|
graph->Release ();
|
|
|
|
|
|
|
|
if (returnSound)
|
|
|
|
{
|
|
|
|
if (GSnd != NULL)
|
|
|
|
{
|
|
|
|
GSnd->MovieResumeSound ();
|
|
|
|
}
|
|
|
|
C_FlushDisplay ();
|
|
|
|
}
|
|
|
|
if (runningFull)
|
|
|
|
{
|
|
|
|
static_cast<Win32Video *> (Video)->GoFullscreen (true);
|
|
|
|
static_cast<BaseWinFB *> (screen)->CreateResources ();
|
|
|
|
}
|
|
|
|
|
|
|
|
bomb1:
|
|
|
|
delete[] uniname;
|
|
|
|
|
|
|
|
if (MovieDestroyed)
|
|
|
|
{
|
|
|
|
exit (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnval;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CheckIfVideo ()
|
|
|
|
{
|
|
|
|
long visible;
|
|
|
|
|
|
|
|
if (vidwin == NULL || video == NULL)
|
|
|
|
{
|
|
|
|
NoVideo = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if (vidwin->get_Visible (&visible) == E_NOINTERFACE)
|
|
|
|
{
|
|
|
|
NoVideo = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
NoVideo = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (NoVideo)
|
|
|
|
{
|
|
|
|
if (vidwin != NULL)
|
|
|
|
{
|
|
|
|
vidwin->Release ();
|
|
|
|
vidwin = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
DHUDMessage *message = new DHUDMessage (
|
|
|
|
"This movie either does not contain a video stream "
|
|
|
|
"or no suitable decompressor could be found.",
|
|
|
|
1.5f, 0.375f, 0, 0, CR_RED, 1.f);
|
|
|
|
StatusBar->AttachMessage (message);
|
|
|
|
screen->Lock (false);
|
|
|
|
StatusBar->Draw (HUD_None);
|
|
|
|
StatusBar->DrawTopStuff (HUD_None);
|
|
|
|
screen->Update ();
|
|
|
|
StatusBar->DetachMessage (message);
|
|
|
|
delete message;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void SetTheVolume ()
|
|
|
|
{
|
|
|
|
if (audio != NULL)
|
|
|
|
{
|
|
|
|
// Convert snd_movievolume from a linear range to 1/100th
|
|
|
|
// decibels and pass that to the IBasicAudio interface.
|
|
|
|
long volume;
|
|
|
|
|
|
|
|
if (snd_movievolume == 0.f)
|
|
|
|
{
|
|
|
|
volume = -10000;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
volume = (long)(log10 (snd_movievolume) * 10000.0);
|
|
|
|
}
|
|
|
|
audio->put_Volume (volume);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void SizeWindowForVideo ()
|
|
|
|
{
|
|
|
|
LONG width, height;
|
|
|
|
|
|
|
|
if (video == NULL || FAILED (video->GetVideoSize (&width, &height)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
SetWindowPos (Window, NULL, 0, 0,
|
|
|
|
width + 2*GetSystemMetrics(SM_CXBORDER),
|
|
|
|
height + GetSystemMetrics(SM_CYCAPTION) + 2*GetSystemMetrics(SM_CYBORDER),
|
|
|
|
SWP_NOMOVE|SWP_NOOWNERZORDER);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void SetMovieSize ()
|
|
|
|
{
|
|
|
|
LONG width, height, left, top;
|
|
|
|
RECT grc;
|
|
|
|
HRESULT hr;
|
|
|
|
|
|
|
|
GetClientRect (Window, &grc);
|
|
|
|
|
|
|
|
hr = video->GetVideoSize (&width, &height);
|
|
|
|
if (FAILED (hr))
|
|
|
|
{
|
|
|
|
vidwin->SetWindowPosition (0, 0, grc.right, grc.bottom);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
double aspect = (double)height / (double)width;
|
|
|
|
|
|
|
|
// Calculate size of video window that fits in our window
|
|
|
|
if (grc.right > grc.bottom)
|
|
|
|
{
|
|
|
|
height = grc.bottom;
|
|
|
|
width = (LONG)((double)height / aspect);
|
|
|
|
}
|
|
|
|
if (grc.right <= grc.bottom || width > grc.right)
|
|
|
|
{
|
|
|
|
width = grc.right;
|
|
|
|
height = (LONG)((double)width * aspect);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then center it
|
|
|
|
left = (grc.right - width) / 2;
|
|
|
|
top = (grc.bottom - height) / 2;
|
|
|
|
|
|
|
|
// And use it
|
|
|
|
vidwin->SetWindowPosition (left, top, width, height);
|
|
|
|
|
|
|
|
InvalidateRect (Window, NULL, FALSE);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // I_DO_NOT_LIKE_BIG_DOWNLOADS
|