mirror of
https://github.com/ddnet/ddnet.git
synced 2024-09-19 17:14:18 +00:00
Added AutoDemoRecord and ghost made by Race mod team, implemented to DDRace by noother
This commit is contained in:
parent
3da1860654
commit
24a688b2c3
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -47,4 +47,5 @@ DDRace-Server*
|
|||
*.user
|
||||
*.cmd
|
||||
.settings
|
||||
*.opensdf
|
||||
|
||||
|
|
|
@ -131,6 +131,13 @@ public:
|
|||
virtual const char *LatestVersion() = 0;
|
||||
virtual bool ConnectionProblems() = 0;
|
||||
|
||||
//DDRace
|
||||
virtual const char* GetCurrentMap() = 0;
|
||||
virtual int GetCurrentMapCrc() = 0;
|
||||
virtual const char* RaceRecordStart(const char *pFilename) = 0;
|
||||
virtual void RaceRecordStop() = 0;
|
||||
virtual bool DemoIsRecording() = 0;
|
||||
|
||||
virtual bool SoundInitFailed() = 0;
|
||||
};
|
||||
|
||||
|
|
|
@ -2354,3 +2354,38 @@ int main(int argc, const char **argv) // ignore_convention
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* CClient::GetCurrentMap()
|
||||
{
|
||||
return m_aCurrentMap;
|
||||
}
|
||||
|
||||
int CClient::GetCurrentMapCrc()
|
||||
{
|
||||
return m_CurrentMapCrc;
|
||||
}
|
||||
|
||||
const char* CClient::RaceRecordStart(const char *pFilename)
|
||||
{
|
||||
char aFilename[128];
|
||||
str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", m_aCurrentMap, pFilename);
|
||||
|
||||
if(State() != STATE_ONLINE)
|
||||
dbg_msg("demorec/record", "client is not online");
|
||||
else
|
||||
m_DemoRecorder.Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_CurrentMapCrc, "client");
|
||||
|
||||
return m_aCurrentMap;
|
||||
}
|
||||
|
||||
void CClient::RaceRecordStop()
|
||||
{
|
||||
if(m_DemoRecorder.IsRecording())
|
||||
m_DemoRecorder.Stop();
|
||||
}
|
||||
|
||||
bool CClient::DemoIsRecording()
|
||||
{
|
||||
return m_DemoRecorder.IsRecording();
|
||||
}
|
||||
|
||||
|
|
|
@ -316,6 +316,13 @@ public:
|
|||
static void Con_Record(IConsole::IResult *pResult, void *pUserData, int ClientID);
|
||||
static void Con_StopRecord(IConsole::IResult *pResult, void *pUserData, int ClientID);
|
||||
|
||||
//DDRace
|
||||
virtual const char* GetCurrentMap();
|
||||
virtual int GetCurrentMapCrc();
|
||||
virtual const char* RaceRecordStart(const char *pFilename);
|
||||
virtual void RaceRecordStop();
|
||||
virtual bool DemoIsRecording();
|
||||
|
||||
void RegisterCommands();
|
||||
|
||||
const char *DemoPlayer_Play(const char *pFilename, int StorageType);
|
||||
|
|
|
@ -193,4 +193,11 @@ MACRO_CONFIG_INT(BrFilterTestServer, br_filter_test_server, 0, 0, 2, CFGFLAG_SAV
|
|||
|
||||
MACRO_CONFIG_INT(SvTeamAskTime, sv_team_ask_time, 10, 0, 9999, CFGFLAG_SERVER, "How much time the player has to accept or refuse", 3)
|
||||
MACRO_CONFIG_INT(SvAllowTeamLeader, sv_allow_team_leader, 1, 0, 1, CFGFLAG_SERVER, "Whether the admin allows teams to have leaders or not", 4)
|
||||
|
||||
MACRO_CONFIG_INT(ClAutoRaceRecord, cl_auto_race_record, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save the best demo of each race", -1)
|
||||
MACRO_CONFIG_INT(ClDemoName, cl_demo_name, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save the playername within the demo", -1)
|
||||
MACRO_CONFIG_INT(ClRaceGhost, cl_race_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Enable ghost",-1)
|
||||
MACRO_CONFIG_INT(ClRaceShowGhost, cl_race_show_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Show ghost",-1)
|
||||
MACRO_CONFIG_INT(ClRaceSaveGhost, cl_race_save_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save ghost",-1)
|
||||
|
||||
#endif
|
||||
|
|
|
@ -63,6 +63,7 @@ public:
|
|||
fs_makedir(GetPath(TYPE_SAVE, "downloadedmaps", aPath, sizeof(aPath)));
|
||||
fs_makedir(GetPath(TYPE_SAVE, "demos", aPath, sizeof(aPath)));
|
||||
fs_makedir(GetPath(TYPE_SAVE, "demos/auto", aPath, sizeof(aPath)));
|
||||
fs_makedir(GetPath(TYPE_SAVE, "ghosts", aPath, sizeof(aPath)));
|
||||
}
|
||||
|
||||
return m_NumPaths ? 0 : 1;
|
||||
|
|
615
src/game/client/components/ghost.cpp
Normal file
615
src/game/client/components/ghost.cpp
Normal file
|
@ -0,0 +1,615 @@
|
|||
/* (c) Rajh, Redix and Sushi. */
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <engine/storage.h>
|
||||
#include <engine/graphics.h>
|
||||
#include <engine/shared/config.h>
|
||||
#include <engine/shared/compression.h>
|
||||
#include <engine/shared/network.h>
|
||||
|
||||
#include <game/generated/client_data.h>
|
||||
#include <game/client/animstate.h>
|
||||
|
||||
#include "skins.h"
|
||||
#include "menus.h"
|
||||
#include "ghost.h"
|
||||
|
||||
/*
|
||||
Note:
|
||||
Freezing fucks up the ghost
|
||||
the ghost isnt really sync
|
||||
don't really get the client tick system for prediction
|
||||
can used PrevChar and PlayerChar and it would be fluent and accurate but won't be predicted
|
||||
so it will be affected by lags
|
||||
*/
|
||||
|
||||
static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0};
|
||||
static const unsigned char gs_ActVersion = 1;
|
||||
|
||||
CGhost::CGhost()
|
||||
{
|
||||
m_lGhosts.clear();
|
||||
m_CurPath.clear();
|
||||
m_CurPos = 0;
|
||||
m_Recording = false;
|
||||
m_Rendering = false;
|
||||
m_RaceState = RACE_NONE;
|
||||
m_NewRecord = false;
|
||||
m_BestTime = -1;
|
||||
m_StartRenderTick = -1;
|
||||
m_StartRecordTick = -1;
|
||||
}
|
||||
|
||||
void CGhost::AddInfos(CNetObj_Character Player)
|
||||
{
|
||||
if(!m_Recording)
|
||||
return;
|
||||
|
||||
|
||||
// Just to be sure it doesnt eat too much memory, the first test should be enough anyway
|
||||
if((Client()->GameTick()-m_StartRecordTick) > Client()->GameTickSpeed()*60*13 || m_CurPath.size() > 50*15*60)
|
||||
{
|
||||
dbg_msg("ghost", "13 minutes elapsed. Stopping ghost record");
|
||||
StopRecord();
|
||||
m_CurPath.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
m_CurPath.add(Player);
|
||||
}
|
||||
|
||||
void CGhost::OnRender()
|
||||
{
|
||||
// only for race
|
||||
if(!m_pClient->m_IsRace || !g_Config.m_ClRaceGhost)
|
||||
return;
|
||||
|
||||
// Check if the race line is crossed then start the render of the ghost if one
|
||||
if(m_RaceState != RACE_STARTED) {
|
||||
bool start = false;
|
||||
|
||||
std::list < int > Indices = m_pClient->Collision()->GetMapIndices(m_pClient->m_PredictedPrevChar.m_Pos, m_pClient->m_LocalCharacterPos);
|
||||
if(!Indices.empty()) {
|
||||
for(std::list < int >::iterator i = Indices.begin(); i != Indices.end(); i++) {
|
||||
if(m_pClient->Collision()->GetTileIndex(*i) == TILE_BEGIN) start = true;
|
||||
}
|
||||
} else {
|
||||
int CurrentIndex = m_pClient->Collision()->GetPureMapIndex(m_pClient->m_LocalCharacterPos);
|
||||
if(m_pClient->Collision()->GetTileIndex(CurrentIndex) == TILE_BEGIN) start = true;
|
||||
}
|
||||
|
||||
if(start) {
|
||||
dbg_msg("ghost", "race started");
|
||||
m_RaceState = RACE_STARTED;
|
||||
StartRender();
|
||||
StartRecord();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(m_RaceState == RACE_FINISHED)
|
||||
{
|
||||
int OwnIndex = -1;
|
||||
for(int i = 0; i < m_lGhosts.size(); i++)
|
||||
if(m_lGhosts[i].m_ID == -1)
|
||||
{
|
||||
OwnIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if(m_NewRecord)
|
||||
{
|
||||
dbg_msg("ghost", "new path saved");
|
||||
m_NewRecord = false;
|
||||
CGhostList Ghost;
|
||||
Ghost.m_ID = -1;
|
||||
Ghost.m_GhostInfo = m_CurInfo;
|
||||
Ghost.m_BestPath.clear();
|
||||
Ghost.m_BestPath = m_CurPath;
|
||||
if(OwnIndex < 0)
|
||||
m_lGhosts.add(Ghost);
|
||||
else
|
||||
m_lGhosts[OwnIndex] = Ghost;
|
||||
|
||||
Save();
|
||||
}
|
||||
StopRecord();
|
||||
StopRender();
|
||||
m_RaceState = RACE_NONE;
|
||||
}
|
||||
|
||||
CNetObj_Character Player = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Cur;
|
||||
m_pClient->m_PredictedChar.Write(&Player);
|
||||
|
||||
if(m_pClient->m_NewPredictedTick)
|
||||
AddInfos(Player);
|
||||
|
||||
// Play the ghost
|
||||
if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
|
||||
return;
|
||||
|
||||
m_CurPos = Client()->PredGameTick()-m_StartRenderTick;
|
||||
|
||||
if(m_lGhosts.size() == 0 || m_CurPos < 0)
|
||||
{
|
||||
//dbg_msg("ghost", "Ghost path done");
|
||||
m_Rendering = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < m_lGhosts.size(); i++)
|
||||
{
|
||||
RenderGhostHook(&m_lGhosts[i]);
|
||||
RenderGhost(&m_lGhosts[i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CGhost::RenderGhost(CGhostList *pGhost)
|
||||
{
|
||||
if(m_CurPos >= pGhost->m_BestPath.size())
|
||||
return;
|
||||
|
||||
CNetObj_Character Player = pGhost->m_BestPath[m_CurPos];
|
||||
CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos];
|
||||
|
||||
if(m_CurPos > 0)
|
||||
Prev = pGhost->m_BestPath[m_CurPos-1];
|
||||
|
||||
char aSkinName[64];
|
||||
IntsToStr(&pGhost->m_GhostInfo.m_Skin0, 6, aSkinName);
|
||||
int SkinId = m_pClient->m_pSkins->Find(aSkinName);
|
||||
if(SkinId < 0)
|
||||
{
|
||||
SkinId = m_pClient->m_pSkins->Find("default");
|
||||
if(SkinId < 0)
|
||||
SkinId = 0;
|
||||
}
|
||||
|
||||
CTeeRenderInfo RenderInfo;
|
||||
RenderInfo.m_ColorBody = m_pClient->m_pSkins->GetColorV4(pGhost->m_GhostInfo.m_ColorBody);
|
||||
RenderInfo.m_ColorFeet = m_pClient->m_pSkins->GetColorV4(pGhost->m_GhostInfo.m_ColorFeet);
|
||||
|
||||
if(pGhost->m_GhostInfo.m_UseCustomColor)
|
||||
RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_ColorTexture;
|
||||
else
|
||||
{
|
||||
RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_OrgTexture;
|
||||
RenderInfo.m_ColorBody = vec4(1,1,1,1);
|
||||
RenderInfo.m_ColorFeet = vec4(1,1,1,1);
|
||||
}
|
||||
|
||||
RenderInfo.m_ColorBody.a = 0.5f;
|
||||
RenderInfo.m_ColorFeet.a = 0.5f;
|
||||
RenderInfo.m_Size = 64;
|
||||
|
||||
float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/256.0f;
|
||||
vec2 Direction = GetDirection((int)(Angle*256.0f));
|
||||
vec2 Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->PredIntraGameTick());
|
||||
vec2 Vel = mix(vec2(Prev.m_VelX/256.0f, Prev.m_VelY/256.0f), vec2(Player.m_VelX/256.0f, Player.m_VelY/256.0f), Client()->PredIntraGameTick());
|
||||
|
||||
bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
|
||||
bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y+16);
|
||||
bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
|
||||
|
||||
float WalkTime = fmod(absolute(Position.x), 100.0f)/100.0f;
|
||||
CAnimState State;
|
||||
State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0);
|
||||
|
||||
if(InAir)
|
||||
State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0, 1.0f);
|
||||
else if(Stationary)
|
||||
State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0, 1.0f);
|
||||
else if(!WantOtherDir)
|
||||
State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
|
||||
|
||||
if (Player.m_Weapon == WEAPON_GRENADE)
|
||||
{
|
||||
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
|
||||
Graphics()->QuadsBegin();
|
||||
Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle*pi*2+Angle);
|
||||
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
|
||||
// normal weapons
|
||||
int iw = clamp(Player.m_Weapon, 0, NUM_WEAPONS-1);
|
||||
RenderTools()->SelectSprite(g_pData->m_Weapons.m_aId[iw].m_pSpriteBody, Direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
|
||||
|
||||
vec2 Dir = Direction;
|
||||
float Recoil = 0.0f;
|
||||
// TODO: is this correct?
|
||||
float a = (Client()->PredGameTick()-Player.m_AttackTick+Client()->PredIntraGameTick())/5.0f;
|
||||
if(a < 1)
|
||||
Recoil = sinf(a*pi);
|
||||
|
||||
vec2 p = Position + Dir * g_pData->m_Weapons.m_aId[iw].m_Offsetx - Direction*Recoil*10.0f;
|
||||
p.y += g_pData->m_Weapons.m_aId[iw].m_Offsety;
|
||||
RenderTools()->DrawSprite(p.x, p.y, g_pData->m_Weapons.m_aId[iw].m_VisualSize);
|
||||
Graphics()->QuadsEnd();
|
||||
}
|
||||
|
||||
// Render ghost
|
||||
RenderTools()->RenderTee(&State, &RenderInfo, 0, Direction, Position, true);
|
||||
}
|
||||
|
||||
void CGhost::RenderGhostHook(CGhostList *pGhost)
|
||||
{
|
||||
if(m_CurPos >= pGhost->m_BestPath.size())
|
||||
return;
|
||||
|
||||
CNetObj_Character Player = pGhost->m_BestPath[m_CurPos];
|
||||
CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos];
|
||||
|
||||
if(m_CurPos > 0)
|
||||
Prev = pGhost->m_BestPath[m_CurPos-1];
|
||||
|
||||
if (Prev.m_HookState<=0 || Player.m_HookState<=0)
|
||||
return;
|
||||
|
||||
float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/256.0f;
|
||||
vec2 Direction = GetDirection((int)(Angle*256.0f));
|
||||
vec2 Pos = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->PredIntraGameTick());
|
||||
|
||||
vec2 HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), Client()->PredIntraGameTick());
|
||||
float d = distance(Pos, HookPos);
|
||||
vec2 Dir = normalize(Pos-HookPos);
|
||||
|
||||
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
|
||||
Graphics()->QuadsBegin();
|
||||
Graphics()->QuadsSetRotation(GetAngle(Dir)+pi);
|
||||
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
|
||||
// render head
|
||||
RenderTools()->SelectSprite(SPRITE_HOOK_HEAD);
|
||||
IGraphics::CQuadItem QuadItem(HookPos.x, HookPos.y, 24, 16);
|
||||
Graphics()->QuadsDraw(&QuadItem, 1);
|
||||
|
||||
// render chain
|
||||
RenderTools()->SelectSprite(SPRITE_HOOK_CHAIN);
|
||||
IGraphics::CQuadItem Array[1024];
|
||||
int j = 0;
|
||||
for(float f = 24; f < d && j < 1024; f += 24, j++)
|
||||
{
|
||||
vec2 p = HookPos + Dir*f;
|
||||
Array[j] = IGraphics::CQuadItem(p.x, p.y, 24, 16);
|
||||
}
|
||||
|
||||
Graphics()->QuadsDraw(Array, j);
|
||||
Graphics()->QuadsSetRotation(0);
|
||||
Graphics()->QuadsEnd();
|
||||
}
|
||||
|
||||
void CGhost::StartRecord()
|
||||
{
|
||||
m_Recording = true;
|
||||
m_CurPath.clear();
|
||||
CNetObj_ClientInfo *pInfo = (CNetObj_ClientInfo *) Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_CLIENTINFO, m_pClient->m_Snap.m_LocalCid);
|
||||
m_CurInfo = *pInfo;
|
||||
m_StartRecordTick = Client()->GameTick();
|
||||
}
|
||||
|
||||
void CGhost::StopRecord()
|
||||
{
|
||||
m_Recording = false;
|
||||
}
|
||||
|
||||
void CGhost::StartRender()
|
||||
{
|
||||
m_CurPos = 0;
|
||||
m_Rendering = true;
|
||||
m_StartRenderTick = Client()->PredGameTick();
|
||||
}
|
||||
|
||||
void CGhost::StopRender()
|
||||
{
|
||||
m_Rendering = false;
|
||||
}
|
||||
|
||||
void CGhost::Save()
|
||||
{
|
||||
if(!g_Config.m_ClRaceSaveGhost)
|
||||
return;
|
||||
|
||||
CGhostHeader Header;
|
||||
|
||||
// check the player name
|
||||
char aName[MAX_NAME_LENGTH];
|
||||
str_copy(aName, g_Config.m_PlayerName, sizeof(aName));
|
||||
for(int i = 0; i < MAX_NAME_LENGTH; i++)
|
||||
{
|
||||
if(!aName[i])
|
||||
break;
|
||||
|
||||
if(aName[i] == '\\' || aName[i] == '/' || aName[i] == '|' || aName[i] == ':' || aName[i] == '*' || aName[i] == '?' || aName[i] == '<' || aName[i] == '>' || aName[i] == '"')
|
||||
aName[i] = '%';
|
||||
}
|
||||
|
||||
char aFilename[256];
|
||||
char aBuf[256];
|
||||
str_format(aFilename, sizeof(aFilename), "%s_%s_%.3f_%08x.gho", Client()->GetCurrentMap(), aName, m_BestTime, Client()->GetCurrentMapCrc());
|
||||
str_format(aBuf, sizeof(aBuf), "ghosts/%s", aFilename);
|
||||
IOHANDLE File = Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
return;
|
||||
|
||||
// write header
|
||||
int Crc = Client()->GetCurrentMapCrc();
|
||||
mem_zero(&Header, sizeof(Header));
|
||||
mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
|
||||
Header.m_Version = gs_ActVersion;
|
||||
IntsToStr(&m_CurInfo.m_Name0, 6, Header.m_aOwner);
|
||||
str_copy(Header.m_aMap, Client()->GetCurrentMap(), sizeof(Header.m_aMap));
|
||||
Header.m_aCrc[0] = (Crc>>24)&0xff;
|
||||
Header.m_aCrc[1] = (Crc>>16)&0xff;
|
||||
Header.m_aCrc[2] = (Crc>>8)&0xff;
|
||||
Header.m_aCrc[3] = (Crc)&0xff;
|
||||
Header.m_Time = m_BestTime;
|
||||
io_write(File, &Header, sizeof(Header));
|
||||
|
||||
// write client info
|
||||
io_write(File, &m_CurInfo, sizeof(m_CurInfo));
|
||||
|
||||
// write data
|
||||
int ItemsPerPackage = 500; // 500 ticks per package
|
||||
int Num = m_CurPath.size();
|
||||
CNetObj_Character *Data = &m_CurPath[0];
|
||||
|
||||
while(Num)
|
||||
{
|
||||
int Items = min(Num, ItemsPerPackage);
|
||||
Num -= Items;
|
||||
|
||||
char aBuffer[100*500];
|
||||
char aBuffer2[100*500];
|
||||
unsigned char aSize[4];
|
||||
|
||||
int Size = sizeof(CNetObj_Character)*Items;
|
||||
mem_copy(aBuffer2, Data, Size);
|
||||
Data += Items;
|
||||
|
||||
Size = CVariableInt::Compress(aBuffer2, Size, aBuffer);
|
||||
Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2));
|
||||
|
||||
aSize[0] = (Size>>24)&0xff;
|
||||
aSize[1] = (Size>>16)&0xff;
|
||||
aSize[2] = (Size>>8)&0xff;
|
||||
aSize[3] = (Size)&0xff;
|
||||
|
||||
io_write(File, aSize, sizeof(aSize));
|
||||
io_write(File, aBuffer2, Size);
|
||||
}
|
||||
|
||||
io_close(File);
|
||||
|
||||
// remove old ghost from list
|
||||
for(int i = 0; i < m_pClient->m_pMenus->m_lGhosts.size(); i++)
|
||||
{
|
||||
CMenus::CGhostItem TmpItem = m_pClient->m_pMenus->m_lGhosts[i];
|
||||
if(TmpItem.m_ID == -1)
|
||||
{
|
||||
char aFile[256];
|
||||
str_format(aFile, sizeof(aFile), "ghosts/%s", TmpItem.m_aFilename);
|
||||
Storage()->RemoveFile(aFile, IStorage::TYPE_SAVE);
|
||||
m_pClient->m_pMenus->m_lGhosts.remove_index(i);
|
||||
break; // TODO: remove other ghosts?
|
||||
}
|
||||
}
|
||||
|
||||
// add new ghost to ghost list
|
||||
CMenus::CGhostItem Item;
|
||||
str_copy(Item.m_aFilename, aFilename, sizeof(Item.m_aFilename));
|
||||
str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer));
|
||||
Item.m_Time = m_BestTime;
|
||||
Item.m_Active = true;
|
||||
Item.m_ID = -1;
|
||||
m_pClient->m_pMenus->m_lGhosts.add(Item);
|
||||
}
|
||||
|
||||
bool CGhost::GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader)
|
||||
{
|
||||
if(!*pFile)
|
||||
return 0;
|
||||
|
||||
CGhostHeader Header;
|
||||
io_read(*pFile, &Header, sizeof(Header));
|
||||
|
||||
*pHeader = Header;
|
||||
|
||||
if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
|
||||
return 0;
|
||||
|
||||
if(Header.m_Version > gs_ActVersion)
|
||||
return 0;
|
||||
|
||||
int Crc = (Header.m_aCrc[0]<<24) | (Header.m_aCrc[1]<<16) | (Header.m_aCrc[2]<<8) | (Header.m_aCrc[3]);
|
||||
if(str_comp(Header.m_aMap, Client()->GetCurrentMap()) != 0 || Crc != Client()->GetCurrentMapCrc())
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool CGhost::GetInfo(const char* pFilename, CGhostHeader *pHeader)
|
||||
{
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename);
|
||||
IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
return 0;
|
||||
|
||||
bool Check = GetHeader(&File, pHeader);
|
||||
io_close(File);
|
||||
|
||||
return Check;
|
||||
}
|
||||
|
||||
void CGhost::Load(const char* pFilename, int ID)
|
||||
{
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename);
|
||||
IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
return;
|
||||
|
||||
// read header
|
||||
CGhostHeader Header;
|
||||
if(!GetHeader(&File, &Header))
|
||||
{
|
||||
io_close(File);
|
||||
return;
|
||||
}
|
||||
|
||||
if(ID == -1)
|
||||
m_BestTime = Header.m_Time;
|
||||
|
||||
// create ghost
|
||||
CGhostList Ghost;
|
||||
Ghost.m_ID = ID;
|
||||
|
||||
// read client info
|
||||
io_read(File, &Ghost.m_GhostInfo, sizeof(Ghost.m_GhostInfo));
|
||||
|
||||
// read data
|
||||
Ghost.m_BestPath.clear();
|
||||
|
||||
while(1)
|
||||
{
|
||||
static char aCompresseddata[100*500];
|
||||
static char aDecompressed[100*500];
|
||||
static char aData[100*500];
|
||||
|
||||
unsigned char aSize[4];
|
||||
if(io_read(File, aSize, sizeof(aSize)) != sizeof(aSize))
|
||||
break;
|
||||
int Size = (aSize[0]<<24) | (aSize[1]<<16) | (aSize[2]<<8) | aSize[3];
|
||||
|
||||
if(io_read(File, aCompresseddata, Size) != (unsigned)Size)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk");
|
||||
break;
|
||||
}
|
||||
|
||||
Size = CNetBase::Decompress(aCompresseddata, Size, aDecompressed, sizeof(aDecompressed));
|
||||
if(Size < 0)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression");
|
||||
break;
|
||||
}
|
||||
|
||||
Size = CVariableInt::Decompress(aDecompressed, Size, aData);
|
||||
if(Size < 0)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression");
|
||||
break;
|
||||
}
|
||||
|
||||
CNetObj_Character *Tmp = (CNetObj_Character*)aData;
|
||||
for(int i = 0; i < (signed)(Size/sizeof(CNetObj_Character)); i++)
|
||||
{
|
||||
Ghost.m_BestPath.add(*Tmp);
|
||||
Tmp++;
|
||||
}
|
||||
}
|
||||
|
||||
io_close(File);
|
||||
|
||||
m_lGhosts.add(Ghost);
|
||||
}
|
||||
|
||||
void CGhost::Unload(int ID)
|
||||
{
|
||||
for(int i = 0; i < m_lGhosts.size(); i++)
|
||||
{
|
||||
if(m_lGhosts[i].m_ID == ID)
|
||||
{
|
||||
m_lGhosts.remove_index_fast(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData, int ClientId)
|
||||
{
|
||||
((CGhost *)pUserData)->StartRender();
|
||||
}
|
||||
|
||||
void CGhost::OnConsoleInit()
|
||||
{
|
||||
Console()->Register("gplay","", CFGFLAG_CLIENT, ConGPlay, this, "", -1);
|
||||
}
|
||||
|
||||
void CGhost::OnMessage(int MsgType, void *pRawMsg)
|
||||
{
|
||||
if(!g_Config.m_ClRaceGhost || m_pClient->m_Snap.m_Spectate)
|
||||
return;
|
||||
|
||||
// only for race
|
||||
if(!m_pClient->m_IsRace)
|
||||
return;
|
||||
|
||||
// check for messages from server
|
||||
if(MsgType == NETMSGTYPE_SV_KILLMSG)
|
||||
{
|
||||
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
|
||||
if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalCid)
|
||||
{
|
||||
OnReset();
|
||||
}
|
||||
}
|
||||
else if(MsgType == NETMSGTYPE_SV_CHAT)
|
||||
{
|
||||
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
|
||||
if(pMsg->m_Cid == -1 && m_RaceState == RACE_STARTED)
|
||||
{
|
||||
const char* pMessage = pMsg->m_pMessage;
|
||||
|
||||
int Num = 0;
|
||||
while(str_comp_num(pMessage, " finished in: ", 14))
|
||||
{
|
||||
pMessage++;
|
||||
Num++;
|
||||
if(!pMessage[0])
|
||||
return;
|
||||
}
|
||||
|
||||
// store the name
|
||||
char aName[64];
|
||||
str_copy(aName, pMsg->m_pMessage, Num+1);
|
||||
|
||||
// prepare values and state for saving
|
||||
int Minutes;
|
||||
float Seconds;
|
||||
if(!str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName) && sscanf(pMessage, " finished in: %d minute(s) %f", &Minutes, &Seconds) == 2)
|
||||
{
|
||||
m_RaceState = RACE_FINISHED;
|
||||
if(m_Recording)
|
||||
{
|
||||
float CurTime = Minutes*60 + Seconds;
|
||||
if(CurTime < m_BestTime || m_BestTime == -1)
|
||||
{
|
||||
m_NewRecord = true;
|
||||
m_BestTime = CurTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CGhost::OnReset()
|
||||
{
|
||||
StopRecord();
|
||||
StopRender();
|
||||
m_RaceState = RACE_NONE;
|
||||
m_NewRecord = false;
|
||||
m_CurPath.clear();
|
||||
m_StartRenderTick = -1;
|
||||
}
|
||||
|
||||
void CGhost::OnMapLoad()
|
||||
{
|
||||
OnReset();
|
||||
m_BestTime = -1;
|
||||
m_lGhosts.clear();
|
||||
m_pClient->m_pMenus->GhostlistPopulate();
|
||||
}
|
80
src/game/client/components/ghost.h
Normal file
80
src/game/client/components/ghost.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
/* (c) Rajh, Redix and Sushi. */
|
||||
|
||||
#ifndef GAME_CLIENT_COMPONENTS_GHOST_H
|
||||
#define GAME_CLIENT_COMPONENTS_GHOST_H
|
||||
|
||||
#include <game/client/component.h>
|
||||
|
||||
class CGhost : public CComponent
|
||||
{
|
||||
public:
|
||||
struct CGhostHeader
|
||||
{
|
||||
unsigned char m_aMarker[8];
|
||||
unsigned char m_Version;
|
||||
char m_aOwner[MAX_NAME_LENGTH];
|
||||
char m_aMap[64];
|
||||
unsigned char m_aCrc[4];
|
||||
float m_Time;
|
||||
};
|
||||
|
||||
private:
|
||||
struct CGhostList
|
||||
{
|
||||
int m_ID;
|
||||
CNetObj_ClientInfo m_GhostInfo;
|
||||
array<CNetObj_Character> m_BestPath;
|
||||
};
|
||||
array<CGhostList> m_lGhosts;
|
||||
|
||||
array<CNetObj_Character> m_CurPath;
|
||||
|
||||
CNetObj_ClientInfo m_CurInfo;
|
||||
|
||||
int m_StartRenderTick;
|
||||
int m_StartRecordTick;
|
||||
int m_CurPos;
|
||||
bool m_Recording;
|
||||
bool m_Rendering;
|
||||
int m_RaceState;
|
||||
float m_BestTime;
|
||||
bool m_NewRecord;
|
||||
|
||||
enum
|
||||
{
|
||||
RACE_NONE = 0,
|
||||
RACE_STARTED,
|
||||
RACE_FINISHED,
|
||||
};
|
||||
|
||||
void AddInfos(CNetObj_Character Player);
|
||||
|
||||
void StartRecord();
|
||||
void StopRecord();
|
||||
void StartRender();
|
||||
void StopRender();
|
||||
void RenderGhost(CGhostList *pGhost);
|
||||
void RenderGhostHook(CGhostList *pGhost);
|
||||
|
||||
bool GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader);
|
||||
|
||||
void Save();
|
||||
|
||||
static void ConGPlay(IConsole::IResult *pResult, void *pUserData, int ClientId);
|
||||
|
||||
public:
|
||||
CGhost();
|
||||
|
||||
virtual void OnRender();
|
||||
virtual void OnConsoleInit();
|
||||
virtual void OnReset();
|
||||
virtual void OnMessage(int MsgType, void *pRawMsg);
|
||||
virtual void OnMapLoad();
|
||||
|
||||
void Load(const char* pFilename, int ID);
|
||||
void Unload(int ID);
|
||||
|
||||
bool GetInfo(const char* pFilename, CGhostHeader *pHeader);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -553,6 +553,15 @@ int CMenus::RenderMenubar(CUIRect r)
|
|||
if(DoButton_MenuTab(&s_ServerInfoButton, Localize("Server info"), m_ActivePage==PAGE_SERVER_INFO, &Button, CUI::CORNER_T))
|
||||
NewPage = PAGE_SERVER_INFO;
|
||||
|
||||
if(m_pClient->m_IsRace)
|
||||
{
|
||||
Box.VSplitLeft(4.0f, 0, &Box);
|
||||
Box.VSplitLeft(100.0f, &Button, &Box);
|
||||
static int s_GhostButton=0;
|
||||
if(DoButton_MenuTab(&s_GhostButton, Localize("Ghost"), m_ActivePage==PAGE_GHOST, &Button, CUI::CORNER_T))
|
||||
NewPage = PAGE_GHOST;
|
||||
}
|
||||
|
||||
Box.VSplitLeft(4.0f, 0, &Box);
|
||||
Box.VSplitLeft(140.0f, &Button, &Box);
|
||||
static int s_CallVoteButton=0;
|
||||
|
@ -788,6 +797,8 @@ int CMenus::Render()
|
|||
RenderGame(MainView);
|
||||
else if(m_GamePage == PAGE_SERVER_INFO)
|
||||
RenderServerInfo(MainView);
|
||||
else if(m_GamePage == PAGE_GHOST)
|
||||
RenderGhost(MainView);
|
||||
else if(m_GamePage == PAGE_CALLVOTE)
|
||||
RenderServerControl(MainView);
|
||||
else if(m_GamePage == PAGE_BROWSER)
|
||||
|
|
|
@ -107,6 +107,7 @@ class CMenus : public CComponent
|
|||
PAGE_NEWS=1,
|
||||
PAGE_GAME,
|
||||
PAGE_SERVER_INFO,
|
||||
PAGE_GHOST,
|
||||
PAGE_CALLVOTE,
|
||||
PAGE_INTERNET,
|
||||
PAGE_LAN,
|
||||
|
@ -180,16 +181,16 @@ class CMenus : public CComponent
|
|||
str_comp_filenames(m_aFilename, Other.m_aFilename) < 0; }
|
||||
};
|
||||
|
||||
sorted_array<CDemoItem> m_lDemos;
|
||||
char m_aCurrentDemoFolder[256];
|
||||
int m_DemolistSelectedIndex;
|
||||
bool m_DemolistSelectedIsDir;
|
||||
int m_DemolistStorageType;
|
||||
|
||||
void DemolistOnUpdate(bool Reset);
|
||||
void DemolistPopulate();
|
||||
static void DemolistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser);
|
||||
|
||||
static void GhostlistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser);
|
||||
|
||||
// found in menus.cpp
|
||||
int Render();
|
||||
//void render_background();
|
||||
|
@ -207,6 +208,7 @@ class CMenus : public CComponent
|
|||
void RenderServerControl(CUIRect MainView);
|
||||
void RenderServerControlKick(CUIRect MainView);
|
||||
void RenderServerControlServer(CUIRect MainView);
|
||||
void RenderGhost(CUIRect MainView);
|
||||
void RenderInGameBrowser(CUIRect MainView);
|
||||
|
||||
// found in menus_browser.cpp
|
||||
|
@ -250,5 +252,25 @@ public:
|
|||
|
||||
//DDRace
|
||||
int DoButton_CheckBox_DontCare(const void *pID, const char *pText, int Checked, const CUIRect *pRect);
|
||||
sorted_array<CDemoItem> m_lDemos;
|
||||
void DemolistPopulate();
|
||||
|
||||
// ghost
|
||||
struct CGhostItem
|
||||
{
|
||||
char m_aFilename[256];
|
||||
char m_aPlayer[MAX_NAME_LENGTH];
|
||||
|
||||
float m_Time;
|
||||
|
||||
bool m_Active;
|
||||
int m_ID;
|
||||
|
||||
bool operator<(const CGhostItem &Other) { return m_Time < Other.m_Time; }
|
||||
};
|
||||
|
||||
sorted_array<CGhostItem> m_lGhosts;
|
||||
|
||||
void GhostlistPopulate();
|
||||
};
|
||||
#endif
|
||||
|
|
|
@ -20,6 +20,10 @@
|
|||
#include "menus.h"
|
||||
#include "motd.h"
|
||||
#include "voting.h"
|
||||
#include <engine/keys.h>
|
||||
#include <engine/graphics.h>
|
||||
#include <engine/storage.h>
|
||||
#include "ghost.h"
|
||||
|
||||
void CMenus::RenderGame(CUIRect MainView)
|
||||
{
|
||||
|
@ -514,3 +518,298 @@ void CMenus::RenderInGameBrowser(CUIRect MainView)
|
|||
RenderServerbrowser(MainView);
|
||||
return;
|
||||
}
|
||||
|
||||
// ghost stuff
|
||||
void CMenus::GhostlistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser)
|
||||
{
|
||||
CMenus *pSelf = (CMenus *)pUser;
|
||||
int Length = str_length(pName);
|
||||
if((pName[0] == '.' && (pName[1] == 0 ||
|
||||
(pName[1] == '.' && pName[2] == 0))) ||
|
||||
(!IsDir && (Length < 4 || str_comp(pName+Length-4, ".gho"))))
|
||||
return;
|
||||
|
||||
CGhost::CGhostHeader Header;
|
||||
if(!pSelf->m_pClient->m_pGhost->GetInfo(pName, &Header))
|
||||
return;
|
||||
|
||||
CGhostItem Item;
|
||||
str_copy(Item.m_aFilename, pName, sizeof(Item.m_aFilename));
|
||||
str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer));
|
||||
Item.m_Time = Header.m_Time;
|
||||
|
||||
Item.m_Active = false;
|
||||
Item.m_ID = pSelf->m_lGhosts.size();
|
||||
|
||||
pSelf->m_lGhosts.add(Item);
|
||||
}
|
||||
|
||||
void CMenus::GhostlistPopulate()
|
||||
{
|
||||
m_lGhosts.clear();
|
||||
Storage()->ListDirectory(IStorage::TYPE_ALL, "ghosts", GhostlistFetchCallback, this);
|
||||
|
||||
int OwnTime = -1;
|
||||
int Own = -1;
|
||||
|
||||
for(int i = 0; i < m_lGhosts.size(); i++)
|
||||
{
|
||||
if(str_comp(m_lGhosts[i].m_aPlayer, g_Config.m_PlayerName) == 0 && (OwnTime == -1 || m_lGhosts[i].m_Time < OwnTime))
|
||||
{
|
||||
OwnTime = m_lGhosts[i].m_Time;
|
||||
Own = i;
|
||||
}
|
||||
}
|
||||
|
||||
if(Own != -1)
|
||||
{
|
||||
m_lGhosts[Own].m_ID = -1;
|
||||
m_lGhosts[Own].m_Active = true;
|
||||
m_pClient->m_pGhost->Load(m_lGhosts[Own].m_aFilename, -1);
|
||||
}
|
||||
}
|
||||
|
||||
void CMenus::RenderGhost(CUIRect MainView)
|
||||
{
|
||||
// render background
|
||||
RenderTools()->DrawUIRect(&MainView, ms_ColorTabbarActive, CUI::CORNER_B|CUI::CORNER_TL, 10.0f);
|
||||
|
||||
MainView.HSplitTop(10.0f, 0, &MainView);
|
||||
MainView.HSplitBottom(5.0f, &MainView, 0);
|
||||
MainView.VSplitLeft(5.0f, 0, &MainView);
|
||||
MainView.VSplitRight(5.0f, &MainView, 0);
|
||||
|
||||
CUIRect Headers, Status;
|
||||
CUIRect View = MainView;
|
||||
|
||||
View.HSplitTop(17.0f, &Headers, &View);
|
||||
View.HSplitBottom(28.0f, &View, &Status);
|
||||
|
||||
// split of the scrollbar
|
||||
RenderTools()->DrawUIRect(&Headers, vec4(1,1,1,0.25f), CUI::CORNER_T, 5.0f);
|
||||
Headers.VSplitRight(20.0f, &Headers, 0);
|
||||
|
||||
struct CColumn
|
||||
{
|
||||
int m_Id;
|
||||
CLocConstString m_Caption;
|
||||
float m_Width;
|
||||
CUIRect m_Rect;
|
||||
CUIRect m_Spacer;
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
COL_ACTIVE=0,
|
||||
COL_NAME,
|
||||
COL_TIME,
|
||||
};
|
||||
|
||||
static CColumn s_aCols[] = {
|
||||
{-1, " ", 2.0f, {0}, {0}},
|
||||
{COL_ACTIVE, " ", 30.0f, {0}, {0}},
|
||||
{COL_NAME, "Name", 300.0f, {0}, {0}},
|
||||
{COL_TIME, "Time", 200.0f, {0}, {0}},
|
||||
};
|
||||
|
||||
int NumCols = sizeof(s_aCols)/sizeof(CColumn);
|
||||
|
||||
// do layout
|
||||
for(int i = 0; i < NumCols; i++)
|
||||
{
|
||||
Headers.VSplitLeft(s_aCols[i].m_Width, &s_aCols[i].m_Rect, &Headers);
|
||||
|
||||
if(i+1 < NumCols)
|
||||
Headers.VSplitLeft(2, &s_aCols[i].m_Spacer, &Headers);
|
||||
}
|
||||
|
||||
// do headers
|
||||
for(int i = 0; i < NumCols; i++)
|
||||
DoButton_GridHeader(s_aCols[i].m_Caption, s_aCols[i].m_Caption, 0, &s_aCols[i].m_Rect);
|
||||
|
||||
RenderTools()->DrawUIRect(&View, vec4(0,0,0,0.15f), 0, 0);
|
||||
|
||||
CUIRect Scroll;
|
||||
View.VSplitRight(15, &View, &Scroll);
|
||||
|
||||
int NumGhosts = m_lGhosts.size();
|
||||
|
||||
int Num = (int)(View.h/s_aCols[0].m_Rect.h) + 1;
|
||||
static int s_ScrollBar = 0;
|
||||
static float s_ScrollValue = 0;
|
||||
|
||||
Scroll.HMargin(5.0f, &Scroll);
|
||||
s_ScrollValue = DoScrollbarV(&s_ScrollBar, &Scroll, s_ScrollValue);
|
||||
|
||||
int ScrollNum = NumGhosts-Num+1;
|
||||
if(ScrollNum > 0)
|
||||
{
|
||||
if(Input()->KeyPresses(KEY_MOUSE_WHEEL_UP))
|
||||
s_ScrollValue -= 1.0f/ScrollNum;
|
||||
if(Input()->KeyPresses(KEY_MOUSE_WHEEL_DOWN))
|
||||
s_ScrollValue += 1.0f/ScrollNum;
|
||||
}
|
||||
else
|
||||
ScrollNum = 0;
|
||||
|
||||
static int s_SelectedIndex = 0;
|
||||
for(int i = 0; i < m_NumInputEvents; i++)
|
||||
{
|
||||
int NewIndex = -1;
|
||||
if(m_aInputEvents[i].m_Flags&IInput::FLAG_PRESS)
|
||||
{
|
||||
if(m_aInputEvents[i].m_Key == KEY_DOWN) NewIndex = s_SelectedIndex + 1;
|
||||
if(m_aInputEvents[i].m_Key == KEY_UP) NewIndex = s_SelectedIndex - 1;
|
||||
}
|
||||
if(NewIndex > -1 && NewIndex < NumGhosts)
|
||||
{
|
||||
//scroll
|
||||
float IndexY = View.y - s_ScrollValue*ScrollNum*s_aCols[0].m_Rect.h + NewIndex*s_aCols[0].m_Rect.h;
|
||||
int Scroll = View.y > IndexY ? -1 : View.y+View.h < IndexY+s_aCols[0].m_Rect.h ? 1 : 0;
|
||||
if(Scroll)
|
||||
{
|
||||
if(Scroll < 0)
|
||||
{
|
||||
int NumScrolls = (View.y-IndexY+s_aCols[0].m_Rect.h-1.0f)/s_aCols[0].m_Rect.h;
|
||||
s_ScrollValue -= (1.0f/ScrollNum)*NumScrolls;
|
||||
}
|
||||
else
|
||||
{
|
||||
int NumScrolls = (IndexY+s_aCols[0].m_Rect.h-(View.y+View.h)+s_aCols[0].m_Rect.h-1.0f)/s_aCols[0].m_Rect.h;
|
||||
s_ScrollValue += (1.0f/ScrollNum)*NumScrolls;
|
||||
}
|
||||
}
|
||||
|
||||
s_SelectedIndex = NewIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if(s_ScrollValue < 0) s_ScrollValue = 0;
|
||||
if(s_ScrollValue > 1) s_ScrollValue = 1;
|
||||
|
||||
// set clipping
|
||||
UI()->ClipEnable(&View);
|
||||
|
||||
CUIRect OriginalView = View;
|
||||
View.y -= s_ScrollValue*ScrollNum*s_aCols[0].m_Rect.h;
|
||||
|
||||
for (int i = 0; i < NumGhosts; i++)
|
||||
{
|
||||
const CGhostItem *pItem = &m_lGhosts[i];
|
||||
CUIRect Row;
|
||||
CUIRect SelectHitBox;
|
||||
|
||||
View.HSplitTop(17.0f, &Row, &View);
|
||||
SelectHitBox = Row;
|
||||
|
||||
// make sure that only those in view can be selected
|
||||
if(Row.y+Row.h > OriginalView.y && Row.y < OriginalView.y+OriginalView.h)
|
||||
{
|
||||
if(i == s_SelectedIndex)
|
||||
{
|
||||
CUIRect r = Row;
|
||||
r.Margin(1.5f, &r);
|
||||
RenderTools()->DrawUIRect(&r, vec4(1,1,1,0.5f), CUI::CORNER_ALL, 4.0f);
|
||||
}
|
||||
|
||||
// clip the selection
|
||||
if(SelectHitBox.y < OriginalView.y) // top
|
||||
{
|
||||
SelectHitBox.h -= OriginalView.y-SelectHitBox.y;
|
||||
SelectHitBox.y = OriginalView.y;
|
||||
}
|
||||
else if(SelectHitBox.y+SelectHitBox.h > OriginalView.y+OriginalView.h) // bottom
|
||||
SelectHitBox.h = OriginalView.y+OriginalView.h-SelectHitBox.y;
|
||||
|
||||
if(UI()->DoButtonLogic(pItem, "", 0, &SelectHitBox))
|
||||
{
|
||||
s_SelectedIndex = i;
|
||||
}
|
||||
|
||||
if(UI()->MouseInside(&Row) && Input()->MouseDoubleClick())
|
||||
{
|
||||
if(m_lGhosts[s_SelectedIndex].m_Active)
|
||||
{
|
||||
m_lGhosts[s_SelectedIndex].m_Active = false;
|
||||
m_pClient->m_pGhost->Unload(m_lGhosts[s_SelectedIndex].m_ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_lGhosts[s_SelectedIndex].m_Active = true;
|
||||
m_pClient->m_pGhost->Load(m_lGhosts[s_SelectedIndex].m_aFilename, m_lGhosts[s_SelectedIndex].m_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int c = 0; c < NumCols; c++)
|
||||
{
|
||||
CUIRect Button;
|
||||
Button.x = s_aCols[c].m_Rect.x;
|
||||
Button.y = Row.y;
|
||||
Button.h = Row.h;
|
||||
Button.w = s_aCols[c].m_Rect.w;
|
||||
|
||||
int Id = s_aCols[c].m_Id;
|
||||
|
||||
if(Id == COL_ACTIVE)
|
||||
{
|
||||
if(pItem->m_Active)
|
||||
{
|
||||
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_EMOTICONS].m_Id);
|
||||
Graphics()->QuadsBegin();
|
||||
RenderTools()->SelectSprite(SPRITE_OOP + 7);
|
||||
IGraphics::CQuadItem QuadItem(Button.x+Button.w/2, Button.y+Button.h/2, 20.0f, 20.0f);
|
||||
Graphics()->QuadsDraw(&QuadItem, 1);
|
||||
|
||||
Graphics()->QuadsEnd();
|
||||
}
|
||||
}
|
||||
else if(Id == COL_NAME)
|
||||
{
|
||||
CTextCursor Cursor;
|
||||
TextRender()->SetCursor(&Cursor, Button.x, Button.y, 12.0f * UI()->Scale(), TEXTFLAG_RENDER|TEXTFLAG_STOP_AT_END);
|
||||
Cursor.m_LineWidth = Button.w;
|
||||
|
||||
char aBuf[128];
|
||||
str_format(aBuf, sizeof(aBuf), "%s%s", pItem->m_aPlayer, (pItem->m_ID == -1)?" (own)":"");
|
||||
TextRender()->TextEx(&Cursor, aBuf, -1);
|
||||
}
|
||||
else if(Id == COL_TIME)
|
||||
{
|
||||
CTextCursor Cursor;
|
||||
TextRender()->SetCursor(&Cursor, Button.x, Button.y, 12.0f * UI()->Scale(), TEXTFLAG_RENDER|TEXTFLAG_STOP_AT_END);
|
||||
Cursor.m_LineWidth = Button.w;
|
||||
|
||||
char aBuf[64];
|
||||
str_format(aBuf, sizeof(aBuf), "%02d:%06.3f", (int)pItem->m_Time/60, pItem->m_Time-((int)pItem->m_Time/60*60));
|
||||
TextRender()->TextEx(&Cursor, aBuf, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UI()->ClipDisable();
|
||||
|
||||
RenderTools()->DrawUIRect(&Status, vec4(1,1,1,0.25f), CUI::CORNER_B, 5.0f);
|
||||
Status.Margin(5.0f, &Status);
|
||||
|
||||
CUIRect Button;
|
||||
Status.VSplitRight(120.0f, &Status, &Button);
|
||||
|
||||
static int s_GhostButton = 0;
|
||||
if(m_lGhosts[s_SelectedIndex].m_Active)
|
||||
{
|
||||
if(DoButton_Menu(&s_GhostButton, Localize("Deactivate"), 0, &Button))
|
||||
{
|
||||
m_lGhosts[s_SelectedIndex].m_Active = false;
|
||||
m_pClient->m_pGhost->Unload(m_lGhosts[s_SelectedIndex].m_ID);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(DoButton_Menu(&s_GhostButton, Localize("Activate"), 0, &Button))
|
||||
{
|
||||
m_lGhosts[s_SelectedIndex].m_Active = true;
|
||||
m_pClient->m_pGhost->Load(m_lGhosts[s_SelectedIndex].m_aFilename, m_lGhosts[s_SelectedIndex].m_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
202
src/game/client/components/race_demo.cpp
Normal file
202
src/game/client/components/race_demo.cpp
Normal file
|
@ -0,0 +1,202 @@
|
|||
/* (c) Redix and Sushi */
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <engine/shared/config.h>
|
||||
#include <engine/serverbrowser.h>
|
||||
#include <engine/storage.h>
|
||||
|
||||
#include "menus.h"
|
||||
#include "race_demo.h"
|
||||
|
||||
CRaceDemo::CRaceDemo()
|
||||
{
|
||||
m_RaceState = RACE_NONE;
|
||||
m_RecordStopTime = 0;
|
||||
m_Time = 0;
|
||||
m_DemoStartTick = 0;
|
||||
}
|
||||
|
||||
void CRaceDemo::OnRender()
|
||||
{
|
||||
if(!g_Config.m_ClAutoRaceRecord || !m_pClient->m_Snap.m_pGameobj || m_pClient->m_Snap.m_Spectate)
|
||||
{
|
||||
m_Active = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active;
|
||||
return;
|
||||
}
|
||||
|
||||
// only for race
|
||||
if(!m_pClient->m_IsRace)
|
||||
return;
|
||||
|
||||
vec2 PlayerPos = m_pClient->m_LocalCharacterPos;
|
||||
|
||||
// start the demo
|
||||
if(((!m_Active && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active)) && m_DemoStartTick < Client()->GameTick())
|
||||
{
|
||||
if(m_RaceState == RACE_STARTED)
|
||||
OnReset();
|
||||
|
||||
m_pMap = Client()->RaceRecordStart("tmp");
|
||||
m_DemoStartTick = Client()->GameTick() + Client()->GameTickSpeed();
|
||||
m_RaceState = RACE_STARTED;
|
||||
}
|
||||
|
||||
// stop the demo
|
||||
if(m_RaceState == RACE_FINISHED && m_RecordStopTime < Client()->GameTick() && m_Time > 0)
|
||||
{
|
||||
CheckDemo();
|
||||
OnReset();
|
||||
}
|
||||
|
||||
m_Active = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active;
|
||||
}
|
||||
|
||||
void CRaceDemo::OnReset()
|
||||
{
|
||||
if(Client()->DemoIsRecording())
|
||||
Client()->RaceRecordStop();
|
||||
|
||||
char aFilename[512];
|
||||
str_format(aFilename, sizeof(aFilename), "demos/%s_tmp.demo", m_pMap);
|
||||
Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE);
|
||||
|
||||
m_Time = 0;
|
||||
m_RaceState = RACE_NONE;
|
||||
m_RecordStopTime = 0;
|
||||
m_DemoStartTick = 0;
|
||||
}
|
||||
|
||||
void CRaceDemo::OnShutdown()
|
||||
{
|
||||
if(Client()->DemoIsRecording())
|
||||
Client()->RaceRecordStop();
|
||||
|
||||
char aFilename[512];
|
||||
str_format(aFilename, sizeof(aFilename), "demos/%s_tmp.demo", m_pMap);
|
||||
Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE);
|
||||
}
|
||||
|
||||
void CRaceDemo::OnMessage(int MsgType, void *pRawMsg)
|
||||
{
|
||||
if(!g_Config.m_ClAutoRaceRecord || m_pClient->m_Snap.m_Spectate)
|
||||
return;
|
||||
|
||||
// only for race
|
||||
if(!m_pClient->m_IsRace)
|
||||
return;
|
||||
|
||||
// check for messages from server
|
||||
if(MsgType == NETMSGTYPE_SV_KILLMSG)
|
||||
{
|
||||
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
|
||||
if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalCid && m_RaceState == RACE_FINISHED)
|
||||
{
|
||||
// check for new record
|
||||
CheckDemo();
|
||||
OnReset();
|
||||
}
|
||||
}
|
||||
else if(MsgType == NETMSGTYPE_SV_CHAT)
|
||||
{
|
||||
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
|
||||
if(pMsg->m_Cid == -1 && m_RaceState == RACE_STARTED)
|
||||
{
|
||||
const char* pMessage = pMsg->m_pMessage;
|
||||
|
||||
int Num = 0;
|
||||
while(str_comp_num(pMessage, " finished in: ", 14))
|
||||
{
|
||||
pMessage++;
|
||||
Num++;
|
||||
if(!pMessage[0])
|
||||
return;
|
||||
}
|
||||
|
||||
// store the name
|
||||
char aName[64];
|
||||
str_copy(aName, pMsg->m_pMessage, Num+1);
|
||||
|
||||
// prepare values and state for saving
|
||||
int Minutes;
|
||||
float Seconds;
|
||||
if(!str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName) && sscanf(pMessage, " finished in: %d minute(s) %f", &Minutes, &Seconds) == 2)
|
||||
{
|
||||
m_RaceState = RACE_FINISHED;
|
||||
m_RecordStopTime = Client()->GameTick() + Client()->GameTickSpeed();
|
||||
m_Time = Minutes*60 + Seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CRaceDemo::CheckDemo()
|
||||
{
|
||||
// stop the demo recording
|
||||
Client()->RaceRecordStop();
|
||||
|
||||
char aTmpDemoName[128];
|
||||
str_format(aTmpDemoName, sizeof(aTmpDemoName), "%s_tmp", m_pMap);
|
||||
|
||||
// loop through demo files
|
||||
m_pClient->m_pMenus->DemolistPopulate();
|
||||
for(int i = 0; i < m_pClient->m_pMenus->m_lDemos.size(); i++)
|
||||
{
|
||||
if(!str_comp_num(m_pClient->m_pMenus->m_lDemos[i].m_aName, m_pMap, str_length(m_pMap)) && str_comp_num(m_pClient->m_pMenus->m_lDemos[i].m_aName, aTmpDemoName, str_length(aTmpDemoName)))
|
||||
{
|
||||
const char *pDemo = m_pClient->m_pMenus->m_lDemos[i].m_aName;
|
||||
|
||||
// set cursor
|
||||
pDemo += str_length(m_pMap)+1;
|
||||
float DemoTime = str_tofloat(pDemo);
|
||||
if(m_Time < DemoTime)
|
||||
{
|
||||
// save new record
|
||||
SaveDemo(m_pMap);
|
||||
|
||||
// delete old demo
|
||||
char aFilename[512];
|
||||
str_format(aFilename, sizeof(aFilename), "demos/%s.demo", m_pClient->m_pMenus->m_lDemos[i].m_aName);
|
||||
Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE);
|
||||
}
|
||||
|
||||
m_Time = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// save demo if there is none
|
||||
SaveDemo(m_pMap);
|
||||
|
||||
m_Time = 0;
|
||||
}
|
||||
|
||||
void CRaceDemo::SaveDemo(const char* pDemo)
|
||||
{
|
||||
char aNewFilename[512];
|
||||
char aOldFilename[512];
|
||||
if(g_Config.m_ClDemoName)
|
||||
{
|
||||
char aPlayerName[MAX_NAME_LENGTH];
|
||||
str_copy(aPlayerName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName, sizeof(aPlayerName));
|
||||
|
||||
// check the player name
|
||||
for(int i = 0; i < MAX_NAME_LENGTH; i++)
|
||||
{
|
||||
if(!aPlayerName[i])
|
||||
break;
|
||||
|
||||
if(aPlayerName[i] == '\\' || aPlayerName[i] == '/' || aPlayerName[i] == '|' || aPlayerName[i] == ':' || aPlayerName[i] == '*' || aPlayerName[i] == '?' || aPlayerName[i] == '<' || aPlayerName[i] == '>' || aPlayerName[i] == '"')
|
||||
aPlayerName[i] = '%';
|
||||
|
||||
str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%5.2f_%s.demo", pDemo, m_Time, aPlayerName);
|
||||
}
|
||||
}
|
||||
else
|
||||
str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%5.2f.demo", pDemo, m_Time);
|
||||
|
||||
str_format(aOldFilename, sizeof(aOldFilename), "demos/%s_tmp.demo", m_pMap);
|
||||
|
||||
Storage()->RenameFile(aOldFilename, aNewFilename, IStorage::TYPE_SAVE);
|
||||
}
|
40
src/game/client/components/race_demo.h
Normal file
40
src/game/client/components/race_demo.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* (c) Redix and Sushi */
|
||||
|
||||
#ifndef GAME_CLIENT_COMPONENTS_RACE_DEMO_H
|
||||
#define GAME_CLIENT_COMPONENTS_RACE_DEMO_H
|
||||
|
||||
#include <game/client/gameclient.h>
|
||||
|
||||
#include <game/client/component.h>
|
||||
|
||||
class CRaceDemo : public CComponent
|
||||
{
|
||||
int m_RecordStopTime;
|
||||
int m_DemoStartTick;
|
||||
float m_Time;
|
||||
const char *m_pMap;
|
||||
|
||||
bool m_Active;
|
||||
|
||||
public:
|
||||
|
||||
int m_RaceState;
|
||||
|
||||
enum
|
||||
{
|
||||
RACE_NONE = 0,
|
||||
RACE_STARTED,
|
||||
RACE_FINISHED,
|
||||
};
|
||||
|
||||
CRaceDemo();
|
||||
|
||||
virtual void OnReset();
|
||||
virtual void OnRender();
|
||||
virtual void OnShutdown();
|
||||
virtual void OnMessage(int MsgType, void *pRawMsg);
|
||||
|
||||
void CheckDemo();
|
||||
void SaveDemo(const char* pDemo);
|
||||
};
|
||||
#endif
|
|
@ -44,6 +44,8 @@
|
|||
#include "components/skins.h"
|
||||
#include "components/sounds.h"
|
||||
#include "components/voting.h"
|
||||
#include "components/race_demo.h"
|
||||
#include "components/ghost.h"
|
||||
#include <base/tl/sorted_array.h>
|
||||
|
||||
CGameClient g_GameClient;
|
||||
|
@ -69,6 +71,8 @@ static CSounds gs_Sounds;
|
|||
static CEmoticon gs_Emoticon;
|
||||
static CDamageInd gsDamageInd;
|
||||
static CVoting gs_Voting;
|
||||
static CRaceDemo gs_RaceDemo;
|
||||
static CGhost gs_Ghost;
|
||||
|
||||
static CPlayers gs_Players;
|
||||
static CNamePlates gs_NamePlates;
|
||||
|
@ -142,6 +146,9 @@ void CGameClient::OnConsoleInit()
|
|||
m_pVoting = &::gs_Voting;
|
||||
m_pScoreboard = &::gs_Scoreboard;
|
||||
|
||||
m_pRaceDemo = &::gs_RaceDemo;
|
||||
m_pGhost = &::gs_Ghost;
|
||||
|
||||
// make a list of all the systems, make sure to add them in the corrent render order
|
||||
m_All.Add(m_pSkins);
|
||||
m_All.Add(m_pMapimages);
|
||||
|
@ -153,11 +160,13 @@ void CGameClient::OnConsoleInit()
|
|||
m_All.Add(m_pSounds);
|
||||
m_All.Add(m_pVoting);
|
||||
m_All.Add(m_pParticles); // doesn't render anything, just updates all the particles
|
||||
m_All.Add(m_pRaceDemo);
|
||||
|
||||
m_All.Add(&gs_MapLayersBackGround); // first to render
|
||||
m_All.Add(&m_pParticles->m_RenderTrail);
|
||||
m_All.Add(&gs_Items);
|
||||
m_All.Add(&gs_Players);
|
||||
m_All.Add(m_pGhost);
|
||||
m_All.Add(&gs_MapLayersForeGround);
|
||||
m_All.Add(&m_pParticles->m_RenderExplosions);
|
||||
m_All.Add(&gs_NamePlates);
|
||||
|
@ -308,6 +317,7 @@ void CGameClient::OnInit()
|
|||
|
||||
m_ServerMode = SERVERMODE_PURE;
|
||||
|
||||
m_IsRace = false;
|
||||
m_DDRaceMsgSent = false;
|
||||
}
|
||||
|
||||
|
@ -397,6 +407,7 @@ void CGameClient::OnReset()
|
|||
m_All.m_paComponents[i]->OnReset();
|
||||
|
||||
m_Teams.Reset();
|
||||
m_IsRace = false;
|
||||
m_DDRaceMsgSent = false;
|
||||
}
|
||||
|
||||
|
@ -623,7 +634,11 @@ void CGameClient::OnStateChange(int NewState, int OldState)
|
|||
m_All.m_paComponents[i]->OnStateChange(NewState, OldState);
|
||||
}
|
||||
|
||||
void CGameClient::OnShutdown() {}
|
||||
void CGameClient::OnShutdown()
|
||||
{
|
||||
m_pRaceDemo->OnShutdown();
|
||||
}
|
||||
|
||||
void CGameClient::OnEnterGame() {}
|
||||
|
||||
void CGameClient::OnGameOver()
|
||||
|
@ -889,6 +904,8 @@ void CGameClient::OnNewSnapshot()
|
|||
CNetMsg_Cl_IsDDRace Msg;
|
||||
Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
|
||||
m_DDRaceMsgSent = true;
|
||||
if(!m_IsRace)
|
||||
m_IsRace = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -218,6 +218,12 @@ public:
|
|||
class CMapImages *m_pMapimages;
|
||||
class CVoting *m_pVoting;
|
||||
class CScoreboard *m_pScoreboard;
|
||||
|
||||
//DDRace
|
||||
//TODO: This is ugly
|
||||
class CRaceDemo *m_pRaceDemo;
|
||||
class CGhost *m_pGhost;
|
||||
bool m_IsRace;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ void CRenderTools::DrawUIRect(const CUIRect *r, vec4 Color, int Corners, float R
|
|||
Graphics()->QuadsEnd();
|
||||
}
|
||||
|
||||
void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos)
|
||||
void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, bool Alpha)
|
||||
{
|
||||
vec2 Direction = Dir;
|
||||
vec2 Position = Pos;
|
||||
|
@ -192,7 +192,10 @@ void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote
|
|||
Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle*pi*2);
|
||||
|
||||
// draw body
|
||||
Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, 1.0f);
|
||||
if(Alpha)
|
||||
Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, pInfo->m_ColorBody.a);
|
||||
else
|
||||
Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, 1.0f);
|
||||
vec2 BodyPos = Position + vec2(pAnim->GetBody()->m_X, pAnim->GetBody()->m_Y)*AnimScale;
|
||||
SelectSprite(OutLine?SPRITE_TEE_BODY_OUTLINE:SPRITE_TEE_BODY, 0, 0, 0);
|
||||
IGraphics::CQuadItem QuadItem(BodyPos.x, BodyPos.y, BaseSize, BaseSize);
|
||||
|
@ -251,7 +254,10 @@ void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote
|
|||
cs = 0.5f;
|
||||
}
|
||||
|
||||
Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, 1.0f);
|
||||
if(Alpha)
|
||||
Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, pInfo->m_ColorFeet.a*cs);
|
||||
else
|
||||
Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, 1.0f);
|
||||
IGraphics::CQuadItem QuadItem(Position.x+pFoot->m_X*AnimScale, Position.y+pFoot->m_Y*AnimScale, w, h);
|
||||
Graphics()->QuadsDraw(&QuadItem, 1);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ public:
|
|||
void RenderTilemapGenerateSkip(class CLayers *pLayers);
|
||||
|
||||
// object render methods (gc_render_obj.cpp)
|
||||
void RenderTee(class CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos);
|
||||
void RenderTee(class CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, bool Alpha=false);
|
||||
|
||||
// map render methods (gc_render_map.cpp)
|
||||
static void RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, float Time, float *pResult);
|
||||
|
|
Loading…
Reference in a new issue