2008-06-01 03:35:47 +00:00
|
|
|
/*
|
|
|
|
** sbar_mugshot.cpp
|
|
|
|
**
|
|
|
|
** Draws customizable mugshots for the status bar.
|
|
|
|
**
|
|
|
|
**---------------------------------------------------------------------------
|
|
|
|
** Copyright 2008 Braden Obrzut
|
|
|
|
** 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.
|
|
|
|
**---------------------------------------------------------------------------
|
|
|
|
**
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "r_defs.h"
|
|
|
|
#include "m_random.h"
|
|
|
|
#include "d_player.h"
|
|
|
|
#include "d_event.h"
|
|
|
|
#include "sbar.h"
|
2008-08-07 07:17:29 +00:00
|
|
|
#include "sbarinfo.h"
|
2009-08-07 04:20:28 +00:00
|
|
|
#include "templates.h"
|
2011-07-06 15:31:05 +00:00
|
|
|
#include "r_utility.h"
|
2008-06-01 03:35:47 +00:00
|
|
|
|
|
|
|
#define ST_RAMPAGEDELAY (2*TICRATE)
|
|
|
|
#define ST_MUCHPAIN 20
|
|
|
|
|
|
|
|
TArray<FMugShotState> MugShotStates;
|
|
|
|
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShotFrame constructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotFrame::FMugShotFrame()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShotFrame destructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotFrame::~FMugShotFrame()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShotFrame :: GetTexture
|
|
|
|
//
|
|
|
|
// Assemble a graphic name with the specified prefix and return the FTexture.
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2010-04-17 02:06:26 +00:00
|
|
|
FTexture *FMugShotFrame::GetTexture(const char *default_face, const char *skin_face, int random, int level,
|
2008-06-01 03:35:47 +00:00
|
|
|
int direction, bool uses_levels, bool health2, bool healthspecial, bool directional)
|
|
|
|
{
|
|
|
|
int index = !directional ? random % Graphic.Size() : direction;
|
|
|
|
if ((unsigned int)index > Graphic.Size() - 1)
|
|
|
|
{
|
|
|
|
index = Graphic.Size() - 1;
|
|
|
|
}
|
2010-07-09 03:04:43 +00:00
|
|
|
FString sprite(skin_face != NULL && skin_face[0] != 0 ? skin_face : default_face, 3);
|
2008-06-01 03:35:47 +00:00
|
|
|
sprite += Graphic[index];
|
|
|
|
if (uses_levels) //change the last character to the level
|
|
|
|
{
|
|
|
|
if (!health2 && (!healthspecial || index == 1))
|
|
|
|
{
|
|
|
|
sprite.LockBuffer()[2 + Graphic[index].Len()] += level;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
sprite.LockBuffer()[1 + Graphic[index].Len()] += level;
|
|
|
|
}
|
|
|
|
sprite.UnlockBuffer();
|
|
|
|
}
|
2013-03-25 18:20:39 +00:00
|
|
|
return TexMan[TexMan.CheckForTexture(sprite, 0, FTextureManager::TEXMAN_TryAny|FTextureManager::TEXMAN_AllowSkins)];
|
2008-06-01 03:35:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// MugShotState default constructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotState::FMugShotState()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// MugShotState named constructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotState::FMugShotState(FName name)
|
|
|
|
{
|
|
|
|
State = name;
|
|
|
|
bUsesLevels = false;
|
|
|
|
bHealth2 = false;
|
|
|
|
bHealthSpecial = false;
|
|
|
|
bDirectional = false;
|
|
|
|
bFinished = true;
|
|
|
|
Random = M_Random();
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// MugShotState destructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotState::~FMugShotState()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShotState :: Tick
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FMugShotState::Tick()
|
|
|
|
{
|
|
|
|
if (Time == -1)
|
|
|
|
{ //When the delay is negative 1, stay on this frame indefinitely.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (Time != 0)
|
|
|
|
{
|
|
|
|
Time--;
|
|
|
|
}
|
|
|
|
else if (Position < Frames.Size() - 1)
|
|
|
|
{
|
|
|
|
Position++;
|
|
|
|
Time = Frames[Position].Delay;
|
|
|
|
Random = M_Random();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
bFinished = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShotState :: Reset
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FMugShotState::Reset()
|
|
|
|
{
|
|
|
|
Time = Frames[0].Delay;
|
|
|
|
Position = 0;
|
|
|
|
bFinished = false;
|
|
|
|
Random = M_Random();
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FindMugShotState
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShotState *FindMugShotState(FName state)
|
|
|
|
{
|
|
|
|
for (unsigned int i = 0; i < MugShotStates.Size(); i++)
|
|
|
|
{
|
|
|
|
if (MugShotStates[i].State == state)
|
|
|
|
return &MugShotStates[i];
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FindMugShotStateIndex
|
|
|
|
//
|
|
|
|
// Used to allow replacements of states
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
int FindMugShotStateIndex(FName state)
|
|
|
|
{
|
|
|
|
for (unsigned int i = 0; i < MugShotStates.Size(); i++)
|
|
|
|
{
|
|
|
|
if (MugShotStates[i].State == state)
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot constructor
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FMugShot::FMugShot()
|
2010-05-21 19:56:13 +00:00
|
|
|
{
|
|
|
|
Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot :: Reset
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FMugShot::Reset()
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
FaceHealth = -1;
|
|
|
|
bEvilGrin = false;
|
|
|
|
bNormal = true;
|
|
|
|
bDamageFaceActive = false;
|
|
|
|
bOuchActive = false;
|
|
|
|
CurrentState = NULL;
|
|
|
|
RampageTimer = 0;
|
|
|
|
LastDamageAngle = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot :: Tick
|
|
|
|
//
|
|
|
|
// Do some stuff related to the mug shot that has to be done at 35fps
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FMugShot::Tick(player_t *player)
|
|
|
|
{
|
|
|
|
if (CurrentState != NULL)
|
|
|
|
{
|
|
|
|
CurrentState->Tick();
|
|
|
|
if (CurrentState->bFinished)
|
|
|
|
{
|
|
|
|
bNormal = true;
|
|
|
|
bOuchActive = false;
|
|
|
|
CurrentState = NULL;
|
|
|
|
}
|
|
|
|
}
|
2008-12-31 12:02:08 +00:00
|
|
|
if ((player->cmd.ucmd.buttons & (BT_ATTACK|BT_ALTATTACK)) && !(player->cheats & (CF_FROZEN | CF_TOTALLYFROZEN)) && player->ReadyWeapon)
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
if (RampageTimer != ST_RAMPAGEDELAY)
|
|
|
|
{
|
|
|
|
RampageTimer++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
RampageTimer = 0;
|
|
|
|
}
|
|
|
|
FaceHealth = player->health;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot :: SetState
|
|
|
|
//
|
|
|
|
// Sets the mug shot state and resets it if it is not the state we are
|
|
|
|
// already on. Wait_till_done is basically a priority variable; when set to
|
2008-06-01 22:41:46 +00:00
|
|
|
// true the state won't change unless the previous state is finished. Reset
|
|
|
|
// overrides the behavior of only switching when the state is not the one we
|
|
|
|
// are already on.
|
2008-06-01 03:35:47 +00:00
|
|
|
// Returns true if the requested state was switched to or is already playing,
|
|
|
|
// and false if the requested state could not be set.
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2008-06-01 22:41:46 +00:00
|
|
|
bool FMugShot::SetState(const char *state_name, bool wait_till_done, bool reset)
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
// Search for full name.
|
|
|
|
FMugShotState *state = FindMugShotState(FName(state_name, true));
|
|
|
|
if (state == NULL)
|
|
|
|
{
|
|
|
|
// Search for initial name, if the full one contains a dot.
|
|
|
|
const char *dot = strchr(state_name, '.');
|
|
|
|
if (dot != NULL)
|
|
|
|
{
|
|
|
|
state = FindMugShotState(FName(state_name, dot - state_name, true));
|
|
|
|
}
|
|
|
|
if (state == NULL)
|
|
|
|
{
|
|
|
|
// Requested state does not exist, so do nothing.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bNormal = false; //Assume we are not setting god or normal for now.
|
|
|
|
bOuchActive = false;
|
|
|
|
if (state != CurrentState)
|
|
|
|
{
|
|
|
|
if (!wait_till_done || CurrentState == NULL || CurrentState->bFinished)
|
|
|
|
{
|
|
|
|
CurrentState = state;
|
|
|
|
state->Reset();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2008-06-01 22:41:46 +00:00
|
|
|
else if(reset)
|
|
|
|
{
|
|
|
|
state->Reset();
|
|
|
|
return true;
|
|
|
|
}
|
2008-06-01 03:35:47 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot :: UpdateState
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2010-12-13 17:09:35 +00:00
|
|
|
CVAR(Bool,st_oldouch,false,CVAR_ARCHIVE)
|
2010-01-01 09:11:55 +00:00
|
|
|
int FMugShot::UpdateState(player_t *player, StateFlags stateflags)
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
int i;
|
|
|
|
angle_t badguyangle;
|
|
|
|
angle_t diffang;
|
|
|
|
FString full_state_name;
|
|
|
|
|
|
|
|
if (player->health > 0)
|
|
|
|
{
|
2010-01-01 09:11:55 +00:00
|
|
|
if (bEvilGrin && !(stateflags & DISABLEGRIN))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
2008-06-16 23:43:31 +00:00
|
|
|
if (player->bonuscount)
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
SetState("grin", false);
|
|
|
|
return 0;
|
|
|
|
}
|
2008-06-16 23:43:31 +00:00
|
|
|
else if (CurrentState == NULL)
|
|
|
|
{
|
|
|
|
bEvilGrin = false;
|
|
|
|
}
|
2008-06-01 03:35:47 +00:00
|
|
|
}
|
|
|
|
|
2010-12-13 17:09:35 +00:00
|
|
|
bool ouch = (!st_oldouch && FaceHealth - player->health > ST_MUCHPAIN) || (st_oldouch && player->health - FaceHealth > ST_MUCHPAIN);
|
2008-08-07 07:17:29 +00:00
|
|
|
if (player->damagecount &&
|
|
|
|
// Now go in if pain is disabled but we think ouch will be shown (and ouch is not disabled!)
|
2010-12-13 17:09:35 +00:00
|
|
|
(!(stateflags & DISABLEPAIN) || (((FaceHealth != -1 && ouch) || bOuchActive) && !(stateflags & DISABLEOUCH))))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
int damage_angle = 1;
|
|
|
|
if (player->attacker && player->attacker != player->mo)
|
|
|
|
{
|
|
|
|
if (player->mo != NULL)
|
|
|
|
{
|
|
|
|
// The next 12 lines are from the Doom statusbar code.
|
|
|
|
badguyangle = R_PointToAngle2(player->mo->x, player->mo->y, player->attacker->x, player->attacker->y);
|
|
|
|
if (badguyangle > player->mo->angle)
|
|
|
|
{
|
|
|
|
// whether right or left
|
|
|
|
diffang = badguyangle - player->mo->angle;
|
|
|
|
i = diffang > ANG180;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// whether left or right
|
|
|
|
diffang = player->mo->angle - badguyangle;
|
|
|
|
i = diffang <= ANG180;
|
|
|
|
} // confusing, aint it?
|
|
|
|
if (i && diffang >= ANG45)
|
|
|
|
{
|
|
|
|
damage_angle = 0;
|
|
|
|
}
|
|
|
|
else if (!i && diffang >= ANG45)
|
|
|
|
{
|
|
|
|
damage_angle = 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bool use_ouch = false;
|
2010-12-13 17:09:35 +00:00
|
|
|
if (((FaceHealth != -1 && ouch) || bOuchActive) && !(stateflags & DISABLEOUCH))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
use_ouch = true;
|
|
|
|
full_state_name = "ouch.";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
full_state_name = "pain.";
|
|
|
|
}
|
|
|
|
full_state_name += player->LastDamageType;
|
2008-06-01 22:41:46 +00:00
|
|
|
if (SetState(full_state_name, false, true))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
bDamageFaceActive = (CurrentState != NULL);
|
|
|
|
LastDamageAngle = damage_angle;
|
|
|
|
bOuchActive = use_ouch;
|
|
|
|
}
|
|
|
|
return damage_angle;
|
|
|
|
}
|
|
|
|
if (bDamageFaceActive)
|
|
|
|
{
|
|
|
|
if (CurrentState == NULL)
|
|
|
|
{
|
|
|
|
bDamageFaceActive = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
bool use_ouch = false;
|
2010-12-13 17:09:35 +00:00
|
|
|
if (((FaceHealth != -1 && ouch) || bOuchActive) && !(stateflags & DISABLEOUCH))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
use_ouch = true;
|
|
|
|
full_state_name = "ouch.";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
full_state_name = "pain.";
|
|
|
|
}
|
|
|
|
full_state_name += player->LastDamageType;
|
|
|
|
if (SetState(full_state_name))
|
|
|
|
{
|
|
|
|
bOuchActive = use_ouch;
|
|
|
|
}
|
|
|
|
return LastDamageAngle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-01-01 09:11:55 +00:00
|
|
|
if (RampageTimer == ST_RAMPAGEDELAY && !(stateflags & DISABLERAMPAGE))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
SetState("rampage", !bNormal); //If we have nothing better to show, use the rampage face.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bNormal)
|
|
|
|
{
|
|
|
|
bool good;
|
|
|
|
if ((player->cheats & CF_GODMODE) || (player->mo != NULL && player->mo->flags2 & MF2_INVULNERABLE))
|
|
|
|
{
|
2010-01-01 09:11:55 +00:00
|
|
|
good = SetState((stateflags & ANIMATEDGODMODE) ? "godanimated" : "god");
|
2008-06-01 03:35:47 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
good = SetState("normal");
|
|
|
|
}
|
|
|
|
if (good)
|
|
|
|
{
|
|
|
|
bNormal = true; //SetState sets bNormal to false.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2010-01-01 09:11:55 +00:00
|
|
|
if (!(stateflags & XDEATHFACE) || !(player->cheats & CF_EXTREMELYDEAD))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
|
|
|
full_state_name = "death.";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
full_state_name = "xdeath.";
|
|
|
|
}
|
|
|
|
full_state_name += player->LastDamageType;
|
|
|
|
SetState(full_state_name);
|
2008-06-16 22:38:23 +00:00
|
|
|
bNormal = true; //Allow the face to return to alive states when the player respawns.
|
2008-06-01 03:35:47 +00:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2008-06-06 23:41:05 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FMugShot :: GetFace
|
|
|
|
//
|
|
|
|
// Updates the status of the mug shot and returns the current face texture.
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2010-01-01 09:11:55 +00:00
|
|
|
FTexture *FMugShot::GetFace(player_t *player, const char *default_face, int accuracy, StateFlags stateflags)
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
2008-08-07 07:17:29 +00:00
|
|
|
int angle = UpdateState(player, stateflags);
|
2008-06-06 23:41:05 +00:00
|
|
|
int level = 0;
|
2009-08-07 04:20:28 +00:00
|
|
|
int max = player->mo->MugShotMaxHealth;
|
|
|
|
if (max < 0)
|
|
|
|
{
|
|
|
|
max = player->mo->GetMaxHealth();
|
|
|
|
}
|
|
|
|
else if (max == 0)
|
|
|
|
{
|
|
|
|
max = 100;
|
|
|
|
}
|
|
|
|
while (player->health < (accuracy - 1 - level) * (max / accuracy))
|
2008-06-01 03:35:47 +00:00
|
|
|
{
|
2008-06-06 23:41:05 +00:00
|
|
|
level++;
|
|
|
|
}
|
|
|
|
if (CurrentState != NULL)
|
|
|
|
{
|
2013-05-12 18:27:03 +00:00
|
|
|
const char *skin_face = player->morphTics ? player->MorphedPlayerClass->Meta.GetMetaString(APMETA_Face) : skins[player->userinfo.GetSkin()].face;
|
2010-04-17 02:06:26 +00:00
|
|
|
return CurrentState->GetCurrentFrameTexture(default_face, skin_face, level, angle);
|
2008-06-01 03:35:47 +00:00
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|