diff --git a/CMakeLists.txt b/CMakeLists.txt index c209c581..1c309ae4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1305,6 +1305,7 @@ set_src(GAME_SHARED GLOB src/game layers.cpp layers.h mapitems.h + resource.h tuning.h variables.h version.h @@ -1416,6 +1417,8 @@ if(CLIENT) components/particles.h components/players.cpp components/players.h + components/resource.cpp + components/resource.h components/scoreboard.cpp components/scoreboard.h components/skins.cpp @@ -1562,6 +1565,8 @@ set_src(GAME_SERVER GLOB_RECURSE src/game/server gameworld.h player.cpp player.h + resource.cpp + resource.h ) set(GAME_GENERATED_SERVER src/generated/server_data.cpp diff --git a/datasrc/datatypes.py b/datasrc/datatypes.py index 6eb340d7..4e4597b2 100644 --- a/datasrc/datatypes.py +++ b/datasrc/datatypes.py @@ -304,6 +304,33 @@ def emit_unpack(self): def emit_unpack_check(self): return [] + +class NetRawData(NetVariable): + def emit_declaration(self): + return [f"const void *{self.name};", f"int {self.name}Size;"] + def emit_unpack(self): + return [f"pMsg->{self.name}Size = pUnpacker->GetInt();", f"pMsg->{self.name} = pUnpacker->GetRaw(pMsg->{self.name}Size);"] + def emit_pack(self): + return [f"pPacker->AddInt({self.name}Size);", f"pPacker->AddRaw({self.name}, {self.name}Size);"] + +class NetRawDataFixed(NetVariable): + def __init__(self, name, data_size, default=None): + NetVariable.__init__(self,name,default=default) + self.data_size = data_size + def emit_declaration(self): + return [f"const void *{self.name};"] + def emit_unpack(self): + return [f"pMsg->{self.name} = pUnpacker->GetRaw({self.data_size});"] + def emit_pack(self): + return [f"pPacker->AddRaw({self.name}, {self.data_size});"] + +class NetRawDataFixedSnapshot(NetVariable): + def __init__(self, name, data_size, default=None): + NetVariable.__init__(self,name,default=default) + self.data_size = data_size + def emit_declaration(self): + return [f"unsigned char {self.name}[{self.data_size}];"] + class NetString(NetVariable): def emit_declaration(self): return ["const char *%s;"%self.name] diff --git a/datasrc/network.py b/datasrc/network.py index c0ac2b24..0fe0ed7e 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -24,8 +24,12 @@ GamePredictionFlags = Flags("GAMEPREDICTIONFLAG", ["EVENT", "INPUT"]) +Resources = Enum("RESOURCE", ["SOUND", "IMAGE"]) + RawHeader = ''' +#include +#include #include #include @@ -77,6 +81,7 @@ Votes, ChatModes, GameMsgIDs, + Resources, ] Flags = [ @@ -276,7 +281,33 @@ NetObjectEx("GameDataPrediction", "game-data-prediction@netobj.teeworlds.wiki", [ NetFlag("m_PredictionFlags", GamePredictionFlags), - ]) + ]), + + NetEventEx("CustomSoundWorld:Common", "custom-sound-world@netevent.teeworlds.wiki", [ + NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)"), + ]), + + NetObjectEx("CustomObject", "custom-entity@netobj.teeworlds.wiki", [ + NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)"), + NetIntAny("m_X"), + NetIntAny("m_Y"), + ]), + + NetObjectEx("CustomImageEntity:CustomObject", "custom-image-entity@netobj.teeworlds.wiki", [ + NetIntAny("m_Angle"), + NetIntAny("m_Width"), + NetIntAny("m_Height"), + ]), + + NetObjectEx("CustomSoundEntity:CustomObject", "custom-sound-entity@netobj.teeworlds.wiki", [ + NetIntAny("m_Vol"), + NetIntAny("m_Distance"), + NetTick("m_StartTick"), + ]), + + NetObjectEx("CharacterGameTexture", "character-game-texture@netobj.teeworlds.wiki", [ + NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)"), + ]), ] Messages = [ @@ -492,4 +523,21 @@ NetStringStrict("m_Arguments") ]), + NetMessageEx("Sv_CustomResource", "custom-resource@netmsg.teeworlds.wiki", [ + NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), + NetEnum("m_Type", Resources), + NetStringStrict("m_Name"), + NetIntAny("m_Crc"), + NetIntRange("m_ChunkPerRequest", 0, 'max_int'), + NetIntRange("m_Size", 0, 'max_int'), + NetRawDataFixed("m_Sha256", "sizeof(SHA256_DIGEST)"), + ]), + NetMessageEx("Sv_CustomResourceData", "custom-resource-data@netmsg.teeworlds.wiki", [ + NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), + NetIntRange("m_ChunkIndex", 0, 'max_int'), + NetRawData("m_Data"), + ]), + NetMessageEx("Cl_ReqeustCustomResource", "request-custom-resource@netmsg.teeworlds.wiki", [ + NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), + ]), ] diff --git a/src/base/uuid.h b/src/base/uuid.h index 1392a05c..a4218780 100644 --- a/src/base/uuid.h +++ b/src/base/uuid.h @@ -46,6 +46,16 @@ inline bool operator!=(const Uuid &that, const Uuid &other) { return !(that == other); } + +inline bool operator<(const Uuid &that, const Uuid &other) +{ + return uuid_comp(that, other) < 0; +} + +inline bool operator<=(const Uuid &that, const Uuid &other) +{ + return uuid_comp(that, other) < 1; +} #endif #endif // BASE_UUID_H diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index cf880dad..ebc15d1d 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -69,6 +69,7 @@ class CStorage : public IStorage fs_makedir(GetPath(TYPE_SAVE, "screenshots/auto", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "maps", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "downloadedmaps", aPath, sizeof(aPath))); + fs_makedir(GetPath(TYPE_SAVE, "downloadedres", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "skins", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "editor", aPath, sizeof(aPath))); } diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp index cbc1b879..cac1b5af 100644 --- a/src/game/client/components/players.cpp +++ b/src/game/client/components/players.cpp @@ -45,7 +45,7 @@ void CPlayers::RenderHook( // draw hook if(Prev.m_HookState > 0 && Player.m_HookState > 0) { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id); + Graphics()->TextureSet(m_pClient->m_Snap.m_aCharacters[ClientID].m_GameTexture); Graphics()->QuadsBegin(); vec2 HookPos; @@ -195,7 +195,7 @@ void CPlayers::RenderPlayer( // draw gun if(Player.m_Weapon >= 0) { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id); + Graphics()->TextureSet(m_pClient->m_Snap.m_aCharacters[ClientID].m_GameTexture); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle * pi * 2 + Angle); diff --git a/src/game/client/components/resource.cpp b/src/game/client/components/resource.cpp new file mode 100644 index 00000000..a9012fba --- /dev/null +++ b/src/game/client/components/resource.cpp @@ -0,0 +1,311 @@ +#include +#include +#include +#include + +#include "resource.h" +#include "sounds.h" + +static void FormatResourcePath(char *pBuffer, int BufferSize, const char *pName, bool Temp, const SHA256_DIGEST *pSha256, const unsigned int *pCrc) +{ + char aSha256[SHA256_MAXSTRSIZE]; + sha256_str(*pSha256, aSha256, sizeof(aSha256)); + if(Temp) + str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.twres.%d.tmp", pName, *pCrc, aSha256, pid()); + else + str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.twres", pName, *pCrc, aSha256); +} + +CClientResManager::CClientResManager() +{ + m_lResources.clear(); +} + +void CClientResManager::RequestDownload(const Uuid *pRequest) +{ + if(!pRequest) + return; + + CNetMsg_Cl_ReqeustCustomResource Msg; + Msg.m_Uuid = pRequest; + Client()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD); +} + +bool CClientResManager::LoadResource(CClientResource *pResource) +{ + IOHANDLE File = Storage()->OpenFile(pResource->m_aPath, IOFLAG_READ, IStorage::TYPE_SAVE, 0, 0, CDataFileReader::CheckSha256, &pResource->m_Sha256); + if(!File) + return false; + pResource->m_pData = (unsigned char *) mem_alloc(pResource->m_DataSize); + io_read(File, pResource->m_pData, pResource->m_DataSize); + io_close(File); + if(pResource->m_Type == RESOURCE_SOUND) + { + pResource->m_Sample = m_pClient->m_pSounds->LoadSampleMemory(pResource->m_aPath, pResource->m_pData, pResource->m_DataSize); + } + else if(pResource->m_Type == RESOURCE_IMAGE) + { + pResource->m_Texture = Graphics()->LoadTexture(pResource->m_aPath, IStorage::TYPE_SAVE, CImageInfo::FORMAT_AUTO, 0); + } + mem_free(pResource->m_pData); + pResource->m_pData = 0; + return true; +} + +void CClientResManager::RenderImageEntity(const CNetObj_CustomImageEntity *pPrev, const CNetObj_CustomImageEntity *pCur) +{ + Uuid TextureID; + mem_copy(&TextureID, pCur->m_Uuid, sizeof(Uuid)); + IGraphics::CTextureHandle Texture = GetResourceTexture(TextureID); + Texture = Texture.IsValid() ? Texture : g_pData->m_aImages[IMAGE_DEADTEE].m_Id; // fallback + vec2 Pos = mix(vec2(pPrev->m_X, pPrev->m_Y), vec2(pCur->m_X, pCur->m_Y), Client()->IntraGameTick()); + vec2 Size = mix(vec2(pPrev->m_Width, pPrev->m_Height), vec2(pCur->m_Width, pCur->m_Height), Client()->IntraGameTick()); + float Angle = mix(pPrev->m_Angle / 256.0f, pCur->m_Angle / 256.0f, Client()->IntraGameTick()); + + Graphics()->BlendNormal(); + Graphics()->TextureSet(Texture); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(Angle); + IGraphics::CQuadItem QuadItem(Pos.x, Pos.y, Size.x, Size.y); + Graphics()->QuadsDraw(&QuadItem, 1); + Graphics()->QuadsEnd(); +} + +void CClientResManager::RenderSoundEntity(const CNetObj_CustomSoundEntity *pPrev, const CNetObj_CustomSoundEntity *pCur, int ItemID) +{ + Uuid SoundID; + mem_copy(&SoundID, pCur->m_Uuid, sizeof(Uuid)); + ISound::CSampleHandle Sample = GetResourceSample(SoundID); + if(!Sample.IsValid()) + return; + // search and mark the entity as active + CSoundEntity *pEntity = 0; + for(int i = 0; i < m_lSoundEntities.size(); i++) + { + if(ItemID == m_lSoundEntities[i].m_SnapshotID) + { + pEntity = &m_lSoundEntities[i]; + pEntity->m_Active = true; + if(mem_comp(pPrev->m_Uuid, pCur->m_Uuid, sizeof(Uuid)) != 0) + { + Sound()->StopVoice(pEntity->m_Voice); + pEntity->m_Voice = Sound()->PlayAt(CSounds::CHN_WORLD, Sample, 1.0f, ISound::FLAG_LOOP, 0, 0); + } + break; + } + } + + int Volume = mix(pPrev->m_Vol, pCur->m_Vol, Client()->IntraGameTick()); + float Distance = mix(pPrev->m_Distance, pCur->m_Distance, Client()->IntraGameTick()) / 256.0f; + vec2 Pos = mix(vec2(pPrev->m_X, pPrev->m_Y), vec2(pCur->m_X, pCur->m_Y), Client()->IntraGameTick()); + if(!pEntity) + { + CSoundEntity &NewEntity = m_lSoundEntities.emplace(); + NewEntity.m_Active = true; + NewEntity.m_SnapshotID = ItemID; + NewEntity.m_Voice = Sound()->PlayAt(CSounds::CHN_WORLD, Sample, 1.0f, ISound::FLAG_LOOP, 0, 0); + pEntity = &NewEntity; + } + + Sound()->SetVoiceVolume(pEntity->m_Voice, Volume / 256.0f); + Sound()->SetVoiceCircle(pEntity->m_Voice, Distance); + Sound()->SetVoicePos(pEntity->m_Voice, Pos.x, Pos.y); + + static float s_Time = 0.0f; + if(m_pClient->m_Snap.m_pGameData) + { + s_Time = mix((Client()->PrevGameTick() - m_pClient->m_Snap.m_pGameData->m_GameStartTick) / (float) Client()->GameTickSpeed(), + (Client()->GameTick() - m_pClient->m_Snap.m_pGameData->m_GameStartTick) / (float) Client()->GameTickSpeed(), + Client()->IntraGameTick()); + } + float Offset = maximum(s_Time - pCur->m_StartTick / (float) Client()->GameTickSpeed(), 0.0f); + Sound()->SetVoiceTimeOffset(pEntity->m_Voice, Offset); +} + +void CClientResManager::OnRender() +{ + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + + for(int i = 0; i < m_lSoundEntities.size(); i++) + { + m_lSoundEntities[i].m_Active = false; + } + + int Num = Client()->SnapNumItems(IClient::SNAP_CURRENT); + for(int i = 0; i < Num; i++) + { + IClient::CSnapItem Item; + const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, i, &Item); + + if(Item.m_Type == NETOBJTYPE_CUSTOMIMAGEENTITY) + { + const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_ID); + if(pPrev) + RenderImageEntity((const CNetObj_CustomImageEntity *) pPrev, (const CNetObj_CustomImageEntity *) pData); + } + else if(Config()->m_SndEnable && Item.m_Type == NETOBJTYPE_CUSTOMSOUNDENTITY) + { + const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_ID); + if(pPrev) + RenderSoundEntity((const CNetObj_CustomSoundEntity *) pPrev, (const CNetObj_CustomSoundEntity *) pData, Item.m_ID); + } + } + + for(int i = 0; i < m_lSoundEntities.size(); i++) + { + if(!m_lSoundEntities[i].m_Active) + { + Sound()->StopVoice(m_lSoundEntities[i].m_Voice); + m_lSoundEntities.remove_index_fast(i); + } + } +} + +void CClientResManager::OnMessage(int MsgType, void *pRawMsg) +{ + if(MsgType == NETMSGTYPE_SV_CUSTOMRESOURCE) + { + CNetMsg_Sv_CustomResource *pMsg = (CNetMsg_Sv_CustomResource *) pRawMsg; + + // protect the player from nasty map names + for(int i = 0; pMsg->m_Name[i]; i++) + { + if(pMsg->m_Name[i] == '/' || pMsg->m_Name[i] == '\\') + { + char aBuf[IO_MAX_PATH_LENGTH + 64]; + str_format(aBuf, sizeof(aBuf), "got strange path from custom resource '%s'", pMsg->m_Name); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "resource", aBuf); + return; + } + } + + if(pMsg->m_Size <= 0) + { + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "resource", "invalid resource size"); + return; + } + + if(FindResource(*static_cast(pMsg->m_Uuid))) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + { + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "resource", "invalid resource uuid"); + return; + } + CClientResource Resource; + str_copy(Resource.m_aName, pMsg->m_Name, sizeof(Resource.m_aName)); + Resource.m_Crc = pMsg->m_Crc; + Resource.m_DataSize = pMsg->m_Size; + Resource.m_Uuid = *static_cast(pMsg->m_Uuid); + Resource.m_pData = 0; + Resource.m_DownloadedSize = 0; + Resource.m_Sample = ISound::CSampleHandle(); + Resource.m_Texture = IGraphics::CTextureHandle(); + Resource.m_Type = pMsg->m_Type; + Resource.m_ChunkPerRequest = pMsg->m_ChunkPerRequest; + mem_copy(&Resource.m_Sha256, pMsg->m_Sha256, sizeof(SHA256_DIGEST)); + FormatResourcePath(Resource.m_aPath, sizeof(Resource.m_aPath), Resource.m_aName, false, &Resource.m_Sha256, &Resource.m_Crc); + FormatResourcePath(Resource.m_aTempPath, sizeof(Resource.m_aTempPath), Resource.m_aName, true, &Resource.m_Sha256, &Resource.m_Crc); + Resource.m_DownloadTemp = 0; + + int Index = m_lResources.add(Resource); + if(!LoadResource(&m_lResources[Index])) + { + m_lResources[Index].m_DownloadTemp = Storage()->OpenFile(m_lResources[Index].m_aTempPath, IOFLAG_WRITE, IStorage::TYPE_SAVE); + RequestDownload(&m_lResources[Index].m_Uuid); + } + } + else if(MsgType == NETMSGTYPE_SV_CUSTOMRESOURCEDATA) + { + CNetMsg_Sv_CustomResourceData *pMsg = (CNetMsg_Sv_CustomResourceData *) pRawMsg; + Uuid TargetResource = *static_cast(pMsg->m_Uuid); + CClientResource *pResource = FindResource(TargetResource); + if(!pResource) + return; + pResource->m_DownloadedSize += pMsg->m_DataSize; + if(pResource->m_DownloadedSize > pResource->m_DataSize) + { + io_close(pResource->m_DownloadTemp); + pResource->m_DownloadTemp = 0; + Storage()->RemoveFile(pResource->m_aTempPath, IStorage::TYPE_SAVE); + m_lResources.remove(*pResource); + return; // invalid! + } + io_write(pResource->m_DownloadTemp, pMsg->m_Data, pMsg->m_DataSize); + if(pResource->m_DownloadedSize == pResource->m_DataSize) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), Localize("Resource '%s': download complete"), pResource->m_aName); + UI()->DoToast(aBuf); + io_close(pResource->m_DownloadTemp); + pResource->m_DownloadTemp = 0; + + Storage()->RemoveFile(pResource->m_aPath, IStorage::TYPE_SAVE); + Storage()->RenameFile(pResource->m_aTempPath, pResource->m_aPath, IStorage::TYPE_SAVE); + + if(!LoadResource(pResource)) + { + Storage()->RemoveFile(pResource->m_aPath, IStorage::TYPE_SAVE); + m_lResources.remove(*pResource); + return; // invalid! + } + } + else if((pMsg->m_ChunkIndex + 1) % pResource->m_ChunkPerRequest == 0) + RequestDownload(&TargetResource); + } +} + +void CClientResManager::OnStateChange(int NewState, int OldState) +{ + if(NewState == IClient::STATE_OFFLINE) + { + if(OldState >= IClient::STATE_CONNECTING && NewState <= IClient::STATE_ONLINE) + { + for(int i = 0; i < m_lResources.size(); i++) + { + if(m_lResources[i].m_Sample.IsValid()) + m_pClient->m_pSounds->UnloadSample(&m_lResources[i].m_Sample); + if(m_lResources[i].m_Texture.IsValid()) + Graphics()->UnloadTexture(&m_lResources[i].m_Texture); + if(m_lResources[i].m_DownloadTemp) + { + io_close(m_lResources[i].m_DownloadTemp); + Storage()->RemoveFile(m_lResources[i].m_aTempPath, IStorage::TYPE_SAVE); + } + } + m_lResources.clear(); + + for(int i = 0; i < m_lSoundEntities.size(); i++) + { + if(m_lSoundEntities[i].m_Active) + Sound()->StopVoice(m_lSoundEntities[i].m_Voice); + } + m_lSoundEntities.clear(); + } + } +} + +CClientResManager::CClientResource *CClientResManager::FindResource(Uuid ResourceID) +{ + for(int i = 0; i < m_lResources.size(); i++) + { + if(m_lResources[i].m_Uuid == ResourceID) + return &m_lResources[i]; + } + return nullptr; +} + +ISound::CSampleHandle CClientResManager::GetResourceSample(Uuid ResID) +{ + CClientResource *pResource = FindResource(ResID); + if(pResource) + return pResource->m_Sample; + return ISound::CSampleHandle(); +} + +IGraphics::CTextureHandle CClientResManager::GetResourceTexture(Uuid ResID) +{ + CClientResource *pResource = FindResource(ResID); + if(pResource) + return pResource->m_Texture; + return IGraphics::CTextureHandle(); +} diff --git a/src/game/client/components/resource.h b/src/game/client/components/resource.h new file mode 100644 index 00000000..e119f542 --- /dev/null +++ b/src/game/client/components/resource.h @@ -0,0 +1,48 @@ +#ifndef GAME_CLIENT_COMPONENTS_RESOURCE_H +#define GAME_CLIENT_COMPONENTS_RESOURCE_H + +#include +#include +#include +#include +#include +#include + +class CClientResManager : public CComponent +{ + struct CClientResource : public CResource + { + char m_aPath[IO_MAX_PATH_LENGTH]; + char m_aTempPath[IO_MAX_PATH_LENGTH]; + int m_DownloadedSize; + int m_ChunkPerRequest; + ISound::CSampleHandle m_Sample; + IGraphics::CTextureHandle m_Texture; + IOHANDLE m_DownloadTemp; + }; + + struct CSoundEntity + { + int m_Voice; + int m_SnapshotID; + bool m_Active; + }; + array m_lSoundEntities; + array m_lResources; + void RequestDownload(const Uuid *pRequest); + bool LoadResource(CClientResource *pResource); + + void RenderImageEntity(const CNetObj_CustomImageEntity *pPrev, const CNetObj_CustomImageEntity *pCur); + void RenderSoundEntity(const CNetObj_CustomSoundEntity *pPrev, const CNetObj_CustomSoundEntity *pCur, int ItemID); +public: + CClientResManager(); + virtual void OnRender(); + virtual void OnMessage(int MsgType, void *pRawMsg); + virtual void OnStateChange(int NewState, int OldState); + CClientResource *FindResource(Uuid ResourceID); + + ISound::CSampleHandle GetResourceSample(Uuid ResID); + IGraphics::CTextureHandle GetResourceTexture(Uuid ResID); +}; + +#endif // GAME_CLIENT_COMPONENTS_RESOURCE_H \ No newline at end of file diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 03a47e82..5204c4ec 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -47,6 +47,7 @@ #include "components/notifications.h" #include "components/particles.h" #include "components/players.h" +#include "components/resource.h" #include "components/scoreboard.h" #include "components/skins.h" #include "components/sounds.h" @@ -119,6 +120,7 @@ static CPlayers gs_Players; static CNamePlates gs_NamePlates; static CItems gs_Items; static CMapImages gs_MapImages; +static CClientResManager gs_ResourceManager; static CMapLayers gs_MapLayersBackGround(CMapLayers::TYPE_BACKGROUND); static CMapLayers gs_MapLayersForeGround(CMapLayers::TYPE_FOREGROUND); @@ -260,6 +262,7 @@ void CGameClient::OnConsoleInit() m_pMapLayersForeGround = &::gs_MapLayersForeGround; m_pMapSounds = &::gs_MapSounds; m_pStats = &::gs_Stats; + m_pResourceManager = &::gs_ResourceManager; // make a list of all the systems, make sure to add them in the corrent render order m_All.Add(m_pSkins); @@ -278,6 +281,7 @@ void CGameClient::OnConsoleInit() m_All.Add(&gs_MapLayersBackGround); // first to render m_All.Add(&m_pParticles->m_RenderTrail); m_All.Add(m_pItems); + m_All.Add(m_pResourceManager); m_All.Add(&gs_Players); m_All.Add(&gs_MapLayersForeGround); m_All.Add(&m_pParticles->m_RenderExplosions); @@ -1118,6 +1122,11 @@ void CGameClient::ProcessEvents() CNetEvent_SoundWorld *ev = (CNetEvent_SoundWorld *) pData; m_pSounds->PlayAt(CSounds::CHN_WORLD, ev->m_SoundID, 1.0f, vec2(ev->m_X, ev->m_Y)); } + else if(Item.m_Type == NETEVENTTYPE_CUSTOMSOUNDWORLD) + { + CNetEvent_CustomSoundWorld *ev = (CNetEvent_CustomSoundWorld *) pData; + m_pSounds->PlaySampleAt(CSounds::CHN_WORLD, m_pResourceManager->GetResourceSample(*reinterpret_cast(&ev->m_Uuid)), 1.0f, vec2(ev->m_X, ev->m_Y)); + } } } @@ -1335,6 +1344,17 @@ void CGameClient::OnNewSnapshot() } } } + else if(Item.m_Type == NETOBJTYPE_CHARACTERGAMETEXTURE) + { + if(Item.m_ID < MAX_CLIENTS) + { + CSnapState::CCharacterInfo *pCharInfo = &m_Snap.m_aCharacters[Item.m_ID]; + Uuid GameTextureID; + mem_copy(&GameTextureID, ((const CNetObj_CharacterGameTexture *) pData)->m_Uuid, sizeof(Uuid)); + pCharInfo->m_UseCustomGameTexture = true; + pCharInfo->m_GameTexture = m_pResourceManager->GetResourceTexture(GameTextureID); + } + } else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO) { m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *) pData; @@ -1470,6 +1490,8 @@ void CGameClient::OnNewSnapshot() EvolveCharacter(&m_Snap.m_aCharacters[i].m_Prev, EvolvePrevTick); if(m_Snap.m_aCharacters[i].m_Cur.m_Tick) EvolveCharacter(&m_Snap.m_aCharacters[i].m_Cur, EvolveCurTick); + if(!m_Snap.m_aCharacters[i].m_UseCustomGameTexture || !m_Snap.m_aCharacters[i].m_GameTexture.IsValid()) + m_Snap.m_aCharacters[i].m_GameTexture = g_pData->m_aImages[IMAGE_GAME].m_Id; m_aClients[i].m_Evolved = m_Snap.m_aCharacters[i].m_Cur; if(i != m_LocalClientID || !Config()->m_ClPredict || Client()->State() == IClient::STATE_DEMOPLAYBACK || !GameDataPredictInput() || !GameDataPredictEvent()) diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 83fb42f4..5da1022d 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -193,6 +193,8 @@ class CGameClient : public IGameClient // interpolated position vec2 m_Position; + bool m_UseCustomGameTexture; + IGraphics::CTextureHandle m_GameTexture; }; CCharacterInfo m_aCharacters[MAX_CLIENTS]; @@ -349,6 +351,7 @@ class CGameClient : public IGameClient class CMapLayers *m_pMapLayersBackGround; class CMapLayers *m_pMapLayersForeGround; class CMapSounds *m_pMapSounds; + class CClientResManager *m_pResourceManager; }; void FormatTime(char *pBuf, int Size, int Time, int Precision); diff --git a/src/game/resource.h b/src/game/resource.h new file mode 100644 index 00000000..5e58e796 --- /dev/null +++ b/src/game/resource.h @@ -0,0 +1,27 @@ +#ifndef GAME_RESOURCE_H +#define GAME_RESOURCE_H + +#include +#include + +struct CResource +{ + enum + { + CHUNK_SIZE = 1200, + }; + + char m_aName[64]; + SHA256_DIGEST m_Sha256; + unsigned m_Crc; + unsigned char *m_pData; + int m_DataSize; + int m_Type; + Uuid m_Uuid; + + bool operator<(const CResource &Other) const { return m_Uuid < Other.m_Uuid; } + bool operator<=(const CResource &Other) const { return m_Uuid <= Other.m_Uuid; } + bool operator==(const CResource &Other) const { return m_Uuid == Other.m_Uuid; } +}; + +#endif // GAME_RESOURCE_H \ No newline at end of file diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 7f18808b..7ee57f6a 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -201,6 +201,21 @@ void CGameContext::CreateSound(vec2 Pos, int Sound, int64 Mask) } } +void CGameContext::CreateCustomSound(vec2 Pos, Uuid Sound, int64 Mask) +{ + if(!ResourceManager()->FindResource(Sound)) + return; + + // create a sound + CNetEvent_CustomSoundWorld *pEvent = (CNetEvent_CustomSoundWorld *) m_Events.Create(NETEVENTTYPE_CUSTOMSOUNDWORLD, sizeof(CNetEvent_CustomSoundWorld), Mask); + if(pEvent) + { + pEvent->m_X = (int) Pos.x; + pEvent->m_Y = (int) Pos.y; + mem_copy(pEvent->m_Uuid, &Sound, sizeof(Uuid)); + } +} + // ----- send functions ----- void CGameContext::SendChat(int ChatterClientID, int Mode, int To, const char *pText) { @@ -559,6 +574,7 @@ void CGameContext::OnTick() { m_apPlayers[i]->Tick(); m_apPlayers[i]->PostTick(); + m_ResourceManager.TrySendResourceInfo(i); } } @@ -746,6 +762,8 @@ void CGameContext::OnClientEnter(int ClientID) Msg.m_Team = NewClientInfoMsg.m_Team; Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, -1); } + + ResourceManager()->OnClientEnter(ClientID); } void CGameContext::OnClientConnected(int ClientID, bool Dummy, bool AsSpec) @@ -1148,6 +1166,11 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID) CNetMsg_Cl_Command *pMsg = (CNetMsg_Cl_Command *) pRawMsg; CommandManager()->OnCommand(pMsg->m_Name, pMsg->m_Arguments, ClientID); } + else if(MsgID == NETMSGTYPE_CL_REQEUSTCUSTOMRESOURCE) + { + CNetMsg_Cl_ReqeustCustomResource *pMsg = (CNetMsg_Cl_ReqeustCustomResource *) pRawMsg; + ResourceManager()->SendResourceData(ClientID, *static_cast(pMsg->m_Uuid)); + } } else { @@ -1721,6 +1744,7 @@ void CGameContext::OnInit() m_World.SetGameServer(this); m_Events.SetGameServer(this); m_CommandManager.Init(m_pConsole, this, NewCommandHook, RemoveCommandHook); + m_ResourceManager.Init(this); // HACK: only set static size for items, which were available in the first 0.7 release // so new items don't break the snapshot delta diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 930f9be2..d744c2c8 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -14,6 +14,7 @@ #include "eventhandler.h" #include "gameworld.h" +#include "resource.h" /* Tick @@ -99,9 +100,11 @@ class CGameContext : public IGameServer class CPlayer *m_apPlayers[MAX_CLIENTS]; class IGameController *m_pController; + CServerResManager m_ResourceManager; CGameWorld m_World; CCommandManager m_CommandManager; + CServerResManager *ResourceManager() { return &m_ResourceManager; } CCommandManager *CommandManager() { return &m_CommandManager; } // helper functions @@ -179,6 +182,7 @@ class CGameContext : public IGameServer void CreatePlayerSpawn(vec2 Pos); void CreateDeath(vec2 Pos, int Who); void CreateSound(vec2 Pos, int Sound, int64 Mask = -1); + void CreateCustomSound(vec2 Pos, Uuid Sound, int64 Mask = -1); // ----- send functions ----- void SendChat(int ChatterClientID, int Mode, int To, const char *pText); diff --git a/src/game/server/gamemodes/infection/reinfected.cpp b/src/game/server/gamemodes/infection/reinfected.cpp index bf878581..8a41f99a 100644 --- a/src/game/server/gamemodes/infection/reinfected.cpp +++ b/src/game/server/gamemodes/infection/reinfected.cpp @@ -402,6 +402,21 @@ void CGameControllerReinfected::OnCharacterSpawn(CCharacter *pChr) } } +void CGameControllerReinfected::Tick() +{ + IGameController::Tick(); + + if(GameServer()->m_World.m_ResetRequested || GameServer()->m_World.m_Paused) + return; + + DoWincheckMatch(); +} + +void CGameControllerReinfected::Snap(int SnappingClient) +{ + IGameController::Snap(SnappingClient); +} + bool CGameControllerReinfected::DoWincheckMatch() { if(GetRealPlayerNum() < Config()->m_RiPlayersMin) diff --git a/src/game/server/gamemodes/infection/reinfected.h b/src/game/server/gamemodes/infection/reinfected.h index 7a795b3b..8f8c761b 100644 --- a/src/game/server/gamemodes/infection/reinfected.h +++ b/src/game/server/gamemodes/infection/reinfected.h @@ -40,6 +40,8 @@ class CGameControllerReinfected : public IGameController virtual int OnCharacterFireWeapon(class CCharacter *pChr, vec2 Direction, int Weapon); virtual void OnCharacterSpawn(class CCharacter *pChr); + virtual void Tick(); + virtual void Snap(int SnappingClient); virtual bool DoWincheckMatch(); virtual void DoTeamChange(class CPlayer *pPlayer, int Team, bool DoChatMsg); diff --git a/src/game/server/resource.cpp b/src/game/server/resource.cpp new file mode 100644 index 00000000..dd603e9d --- /dev/null +++ b/src/game/server/resource.cpp @@ -0,0 +1,151 @@ +#include +#include +#include +#include + +#include "gamecontext.h" +#include "resource.h" + +IConsole *CServerResManager::Console() { return m_pGameContext->Console(); } +IServer *CServerResManager::Server() { return m_pGameContext->Server(); } +IStorage *CServerResManager::Storage() { return m_pGameContext->Storage(); } +CConfig *CServerResManager::Config() { return m_pGameContext->Config(); } + +CServerResManager::CServerResource *CServerResManager::FindResource(Uuid ResourceID) +{ + for(int i = 0; i < m_lResources.size(); i++) + { + if(m_lResources[i].m_Uuid == ResourceID) + return &m_lResources[i]; + } + return nullptr; +} + +CServerResManager::CServerResManager() +{ +} + +CServerResManager::~CServerResManager() +{ + Clear(); +} + +void CServerResManager::Init(CGameContext *pGameContext) +{ + m_pGameContext = pGameContext; + m_ChunksPerRequest = Config()->m_SvResDownloadSpeed; +} + +void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) +{ + int ChunkSize = CResource::CHUNK_SIZE; + CServerResource *pTarget = FindResource(RequestUuid); + if(!pTarget) + return; + + // send resource chunks, copied from map download + for(int i = 0; i < Config()->m_SvResDownloadSpeed && pTarget->m_aDownloadChunks[ClientID] >= 0; ++i) + { + int Chunk = pTarget->m_aDownloadChunks[ClientID]; + int Offset = Chunk * ChunkSize; + + // check for last part + if(Offset + ChunkSize >= pTarget->m_DataSize) + { + ChunkSize = pTarget->m_DataSize - Offset; + pTarget->m_aDownloadChunks[ClientID] = -1; + } + else + pTarget->m_aDownloadChunks[ClientID]++; + + CNetMsg_Sv_CustomResourceData Msg; + Msg.m_Uuid = &pTarget->m_Uuid; + Msg.m_ChunkIndex = Chunk; + Msg.m_Data = &pTarget->m_pData[Offset]; + Msg.m_DataSize = ChunkSize; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD, ClientID); + + if(Config()->m_Debug) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "sending chunk %d with size %d", Chunk, ChunkSize); + Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "resource", aBuf); + } + } +} + +void CServerResManager::AddResource(const char *pPath, const char *pName, const Uuid ResourceID) +{ + char aBuf[256]; + if(!str_endswith(pPath, ".png") && !str_endswith(pPath, ".opus")) + { + str_format(aBuf, sizeof(aBuf), "failed to load resource with wrong extension '%s'(%s)", pName, pPath); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "resource", aBuf); + return; + } + + IOHANDLE File = Storage()->OpenFile(pPath, IOFLAG_READ, IStorage::TYPE_ALL); + if(!File) + { + str_format(aBuf, sizeof(aBuf), "failed to load resource '%s'(%s)", pName, pPath); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "resource", aBuf); + return; + } + CServerResource NewRes; + unsigned int DataSize; + (void) Storage()->GetHashAndSize(pPath, IStorage::TYPE_ALL, &NewRes.m_Sha256, &NewRes.m_Crc, &DataSize); + str_copy(NewRes.m_aName, pName, sizeof(NewRes.m_aName)); + NewRes.m_DataSize = static_cast(DataSize); + NewRes.m_Type = str_endswith(pPath, ".png") ? RESOURCE_IMAGE : RESOURCE_SOUND; + NewRes.m_Uuid = ResourceID; + NewRes.m_pData = (unsigned char *) mem_alloc(NewRes.m_DataSize); + io_read(File, NewRes.m_pData, NewRes.m_DataSize); + io_close(File); + m_lResources.add(NewRes); + + str_format(aBuf, sizeof(aBuf), "loaded resource '%s'(%s)", pName, pPath); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "resource", aBuf); +} + +void CServerResManager::OnClientEnter(int ClientID) +{ + if(Server()->GetClientVersion(ClientID) < 0x0706) + return; + m_aResourceSendIndex[ClientID] = 0; +} + +void CServerResManager::Clear() +{ + for(int i = 0; i < m_lResources.size(); i++) + { + if(m_lResources[i].m_pData) + mem_free(m_lResources[i].m_pData); + m_lResources[i].m_pData = 0; + } + for(int i = 0; i < MAX_CLIENTS; i++) + { + m_aResourceSendIndex[i] = -1; + } +} + +void CServerResManager::TrySendResourceInfo(int ClientID) +{ + if(!GameServer()->m_apPlayers[ClientID]) + return; + + int Index = m_aResourceSendIndex[ClientID]; + if(Index < 0 || Index >= m_lResources.size()) + return; + CNetMsg_Sv_CustomResource Resource; + Resource.m_Uuid = &m_lResources[Index].m_Uuid; + Resource.m_Type = m_lResources[Index].m_Type; + Resource.m_Name = m_lResources[Index].m_aName; + Resource.m_Crc = m_lResources[Index].m_Crc; + Resource.m_Sha256 = &m_lResources[Index].m_Sha256; + Resource.m_Size = m_lResources[Index].m_DataSize; + Resource.m_ChunkPerRequest = m_ChunksPerRequest; + Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD, ClientID); + + m_lResources[Index].m_aDownloadChunks[ClientID] = 0; + m_aResourceSendIndex[ClientID]++; +} diff --git a/src/game/server/resource.h b/src/game/server/resource.h new file mode 100644 index 00000000..86f72275 --- /dev/null +++ b/src/game/server/resource.h @@ -0,0 +1,38 @@ +#ifndef GAME_SERVER_RESOURCE_H +#define GAME_SERVER_RESOURCE_H + +#include +#include + +class CServerResManager +{ + class CGameContext *m_pGameContext; + class CGameContext *GameServer() const { return m_pGameContext; } + class IConsole *Console(); + class IServer *Server(); + class IStorage *Storage(); + class CConfig *Config(); + + struct CServerResource : public CResource + { + int m_aDownloadChunks[MAX_CLIENTS]; + }; + + int m_aResourceSendIndex[MAX_CLIENTS]; + array m_lResources; + int m_ChunksPerRequest; + +public: + CServerResManager(); + ~CServerResManager(); + + void Init(class CGameContext *pGameContext); + void AddResource(const char *pPath, const char *pName, const Uuid ResourceID); + void SendResourceData(int ClientID, const Uuid RequestUuid); + void OnClientEnter(int ClientID); + void Clear(); + void TrySendResourceInfo(int ClientID); + CServerResource *FindResource(Uuid ResourceID); +}; + +#endif // GAME_SERVER_RESOURCE_H \ No newline at end of file diff --git a/src/game/variables.h b/src/game/variables.h index 70b4b0ca..82ea1aed 100644 --- a/src/game/variables.h +++ b/src/game/variables.h @@ -143,6 +143,8 @@ MACRO_CONFIG_INT(SvVoteKickMin, sv_vote_kick_min, 0, 0, MAX_CLIENTS, CFGFLAG_SAV MACRO_CONFIG_INT(SvVoteKickBantime, sv_vote_kick_bantime, 5, 0, 1440, CFGFLAG_SAVE | CFGFLAG_SERVER, "The time to ban a player if kicked by vote. 0 makes it just use kick") MACRO_CONFIG_INT(SvAllowSpecVoting, sv_allow_spec_voting, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_SERVER, "Allow voting by spectators") +MACRO_CONFIG_INT(SvResDownloadSpeed, sv_res_download_speed, 8, 1, 16, CFGFLAG_SAVE | CFGFLAG_SERVER, "Number of custom resource data packages a client gets on each request") + MACRO_CONFIG_INT(RiPlayersMin, ri_players_min, 2, 0, MAX_CLIENTS, CFGFLAG_SAVE | CFGFLAG_SERVER, "Minimum number of players required to start reinfected game") MACRO_CONFIG_INT(RiInfectionStartTime, ri_infection_start_time, 10, 0, MAX_CLIENTS, CFGFLAG_SAVE | CFGFLAG_SERVER, "timer for reinfected game to start infection") MACRO_CONFIG_INT(RiWallLength, ri_walllength, 256, 128, 1024, CFGFLAG_SERVER, "Length of a reinfected wall")