/* (c) Rajh, Redix and Sushi. */ #include #include #include #include #include #include #include #include "race.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 = 2; CGhost::CGhost() { m_lGhosts.clear(); m_CurGhost.m_Path.clear(); m_CurGhost.m_ID = -1; m_CurPos = 0; m_Recording = false; m_Rendering = false; m_RaceState = RACE_NONE; m_NewRecord = false; m_BestTime = -1; m_StartRenderTick = -1; } void CGhost::AddInfos(CGhostCharacter Player) { if(!m_Recording) return; // Just to be sure it doesnt eat too much memory, the first test should be enough anyway if(m_CurGhost.m_Path.size() > Client()->GameTickSpeed()*60*20) { dbg_msg("ghost", "20 minutes elapsed. stopping ghost record"); StopRecord(); m_CurGhost.m_Path.clear(); return; } m_CurGhost.m_Path.add(Player); } void CGhost::OnRender() { if(!g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE) return; // Check if the race line is crossed then start the render of the ghost if one 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 { Start = m_pClient->Collision()->GetTileIndex(m_pClient->Collision()->GetPureMapIndex(m_pClient->m_LocalCharacterPos)) == TILE_BEGIN; } if(Start) { OnReset(); m_RaceState = RACE_STARTED; StartRender(); StartRecord(); } if(m_RaceState == RACE_FINISHED) { if(m_NewRecord) { // search for own ghost array::range r = find_linear(m_lGhosts.all(), m_CurGhost); m_NewRecord = false; if(r.empty()) m_lGhosts.add(m_CurGhost); else r.front() = m_CurGhost; Save(); } StopRecord(); StopRender(); m_RaceState = RACE_NONE; } CNetObj_Character Char = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_Cur; m_pClient->m_PredictedChar.Write(&Char); if(m_pClient->m_NewPredictedTick) AddInfos(GetGhostCharacter(Char)); // 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) { StopRender(); return; } for(int i = 0; i < m_lGhosts.size(); i++) { CGhostItem *pGhost = &m_lGhosts[i]; if(m_CurPos >= pGhost->m_Path.size()) continue; int PrevPos = (m_CurPos > 0) ? m_CurPos-1 : m_CurPos; CGhostCharacter Player = pGhost->m_Path[m_CurPos]; CGhostCharacter Prev = pGhost->m_Path[PrevPos]; CNetObj_ClientInfo Info = pGhost->m_Info; RenderGhostHook(Player, Prev); RenderGhost(Player, Prev, Info); } } void CGhost::RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_ClientInfo Info) { char aSkinName[64]; IntsToStr(&Info.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(Info.m_ColorBody); RenderInfo.m_ColorFeet = m_pClient->m_pSkins->GetColorV4(Info.m_ColorFeet); if(Info.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 IntraTick = Client()->PredIntraGameTick(); float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, IntraTick)/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), IntraTick); 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), IntraTick); 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+IntraTick)/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(CGhostCharacter Player, CGhostCharacter Prev) { if (Prev.m_HookState<=0 || Player.m_HookState<=0) return; float IntraTick = Client()->PredIntraGameTick(); vec2 Pos = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick); vec2 HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), IntraTick); 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(); } CGhost::CGhostCharacter CGhost::GetGhostCharacter(CNetObj_Character Char) { CGhostCharacter Player; Player.m_X = Char.m_X; Player.m_Y = Char.m_Y; Player.m_VelX = Char.m_VelX; Player.m_VelY = Char.m_VelY; Player.m_Angle = Char.m_Angle; Player.m_Direction = Char.m_Direction; Player.m_Weapon = Char.m_Weapon; Player.m_HookState = Char.m_HookState; Player.m_HookX = Char.m_HookX; Player.m_HookY = Char.m_HookY; Player.m_AttackTick = Char.m_AttackTick; return Player; } void CGhost::StartRecord() { m_Recording = true; m_CurGhost.m_Path.clear(); CNetObj_ClientInfo *pInfo = (CNetObj_ClientInfo *) Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_CLIENTINFO, m_pClient->m_Snap.m_LocalClientID); if (pInfo) m_CurGhost.m_Info = *pInfo; } 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]; str_format(aFilename, sizeof(aFilename), "ghosts/%s_%s_%d.%03d_%08x.gho", Client()->GetCurrentMap(), aName, m_BestTime / 1000, m_BestTime % 1000, Client()->GetCurrentMapCrc()); IOHANDLE File = Storage()->OpenFile(aFilename, 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_CurGhost.m_Info.m_Name0, 4, 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 / 1000.f; Header.m_NumShots = m_CurGhost.m_Path.size(); io_write(File, &Header, sizeof(Header)); // write client info io_write(File, &m_CurGhost.m_Info, sizeof(m_CurGhost.m_Info)); // write data int ItemsPerPackage = 500; // 500 ticks per package int Num = Header.m_NumShots; CGhostCharacter *Data = &m_CurGhost.m_Path[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(CGhostCharacter)*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 (TODO: remove other ghosts?) if(m_pClient->m_pMenus->m_OwnGhost) { char aFile[256]; str_format(aFile, sizeof(aFile), "ghosts/%s", m_pClient->m_pMenus->m_OwnGhost->m_aFilename); Storage()->RemoveFile(aFile, IStorage::TYPE_SAVE); m_pClient->m_pMenus->m_lGhosts.remove(*m_pClient->m_pMenus->m_OwnGhost); } 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); m_pClient->m_pMenus->m_OwnGhost = &find_linear(m_pClient->m_pMenus->m_lGhosts.all(), Item).front(); dbg_msg("ghost", "saved better ghost"); m_Saving = false; } 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 * 1000; int NumShots = Header.m_NumShots; // create ghost CGhostItem Ghost; Ghost.m_ID = ID; Ghost.m_Path.clear(); Ghost.m_Path.set_size(NumShots); // read client info io_read(File, &Ghost.m_Info, sizeof(Ghost.m_Info)); // read data int Index = 0; while(Index < NumShots) { 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; } CGhostCharacter *Tmp = (CGhostCharacter*)aData; for(unsigned i = 0; i < Size/sizeof(CGhostCharacter); i++) { if(Index >= NumShots) break; Ghost.m_Path[Index] = *Tmp; Index++; Tmp++; } } io_close(File); m_lGhosts.add(Ghost); } void CGhost::Unload(int ID) { CGhostItem Item; Item.m_ID = ID; m_lGhosts.remove_fast(Item); } void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData) { ((CGhost *)pUserData)->StartRender(); } void CGhost::OnConsoleInit() { Console()->Register("gplay", "", CFGFLAG_CLIENT, ConGPlay, this, ""); } void CGhost::OnMessage(int MsgType, void *pRawMsg) { if(!g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE || m_pClient->m_Snap.m_SpecInfo.m_Active) 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_LocalClientID) { if(!m_Saving) OnReset(); } } else if(MsgType == NETMSGTYPE_SV_CHAT) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; if(pMsg->m_ClientID == -1 && m_RaceState == RACE_STARTED) { char aName[MAX_NAME_LENGTH]; int Time = CRaceHelper::TimeFromFinishMessage(pMsg->m_pMessage, aName, sizeof(aName)); if(Time > 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName) == 0) { m_RaceState = RACE_FINISHED; if(m_Recording && (Time < m_BestTime || m_BestTime == -1)) { m_NewRecord = true; m_BestTime = Time; m_Saving = true; } } } } } void CGhost::OnReset() { StopRecord(); StopRender(); m_RaceState = RACE_NONE; m_NewRecord = false; m_CurGhost.m_Path.clear(); m_StartRenderTick = -1; m_Saving = false; } void CGhost::OnMapLoad() { OnReset(); m_BestTime = -1; m_lGhosts.clear(); m_pClient->m_pMenus->GhostlistPopulate(); }