diff --git a/CMakeLists.txt b/CMakeLists.txt
index d26e8d06..56b04278 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1306,6 +1306,7 @@ set_src(GAME_SHARED GLOB src/game
layers.cpp
layers.h
mapitems.h
+ resource.h
tuning.h
variables.h
version.h
@@ -1415,6 +1416,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
@@ -1559,6 +1562,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 c6780fca..e4c99b2f 100644
--- a/datasrc/network.py
+++ b/datasrc/network.py
@@ -22,8 +22,12 @@
GamePredictionFlags = Flags("GAMEPREDICTIONFLAG", ["EVENT", "INPUT"])
+Resources = Enum("RESOURCE", ["SOUND", "IMAGE"])
+
RawHeader = '''
+#include
+#include
#include
#include
@@ -75,6 +79,7 @@
Votes,
ChatModes,
GameMsgIDs,
+ Resources,
]
Flags = [
@@ -274,7 +279,32 @@
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_Offset"),
+ ]),
+
+ NetObjectEx("CharacterGameTexture", "character-game-texture@netobj.teeworlds.wiki", [
+ NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)"),
+ ]),
]
Messages = [
@@ -490,4 +520,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/client/sound.cpp b/src/engine/client/sound.cpp
index 442dcc0e..f12041ad 100644
--- a/src/engine/client/sound.cpp
+++ b/src/engine/client/sound.cpp
@@ -332,11 +332,10 @@ void CSound::RateConvert(int SampleID)
pSample->m_NumFrames = NumFrames;
}
-ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
+ISound::CSampleHandle CSound::LoadOpusMemory(const char *pContext, const unsigned char *pData, int DataSize)
{
CSample *pSample;
int SampleID = -1;
-
// don't waste memory on sound when we are stress testing
#ifdef CONF_DEBUG
if(m_pConfig->m_DbgStress)
@@ -349,23 +348,16 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
if(!m_pStorage)
return CSampleHandle();
-
- lock_wait(m_SoundLock);
- unsigned char *pFileData;
- unsigned FileSize;
- if(!m_pStorage->ReadFile(pFilename, IStorage::TYPE_ALL, (void **) &pFileData, &FileSize))
- {
- dbg_msg("sound/opus", "failed to open file. filename='%s'", pFilename);
- lock_unlock(m_SoundLock);
+
+ if(!pContext || !pData || DataSize <= 0)
return CSampleHandle();
- }
+ lock_wait(m_SoundLock);
int Error = 0;
- OggOpusFile *pOpusFile = op_open_memory(pFileData, FileSize, &Error);
+ OggOpusFile *pOpusFile = op_open_memory(pData, DataSize, &Error);
if(!pOpusFile)
{
- dbg_msg("sound/opus", "failed to open opus file '%s': error %d", pFilename, Error);
- mem_free(pFileData);
+ dbg_msg("sound/opus", "failed to open opus '%s': error %d", pContext, Error);
lock_unlock(m_SoundLock);
return CSampleHandle();
}
@@ -374,9 +366,8 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
;
if(NumChannels < 0 || NumChannels > 2)
{
- dbg_msg("sound/opus", "only mono/stereo supported. channels=%d, file='%s'", NumChannels, pFilename);
+ dbg_msg("sound/opus", "only mono/stereo supported. channels=%d, file='%s'", NumChannels, pContext);
op_free(pOpusFile);
- mem_free(pFileData);
lock_unlock(m_SoundLock);
return CSampleHandle();
}
@@ -384,9 +375,8 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
int TotalSamples = op_pcm_total(pOpusFile, -1);
if(TotalSamples < 0)
{
- dbg_msg("sound/opus", "failed to get number of samples, error %d. file='%s'", TotalSamples, pFilename);
+ dbg_msg("sound/opus", "failed to get number of samples, error %d. file='%s'", TotalSamples, pContext);
op_free(pOpusFile);
- mem_free(pFileData);
lock_unlock(m_SoundLock);
return CSampleHandle();
}
@@ -395,7 +385,6 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
if(SampleID < 0)
{
op_free(pOpusFile);
- mem_free(pFileData);
lock_unlock(m_SoundLock);
return CSampleHandle();
}
@@ -404,7 +393,6 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
if(!pSample->m_pData)
{
op_free(pOpusFile);
- mem_free(pFileData);
lock_unlock(m_SoundLock);
return CSampleHandle();
}
@@ -417,8 +405,7 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
{
mem_free(pSample->m_pData);
op_free(pOpusFile);
- mem_free(pFileData);
- dbg_msg("sound/opus", "op_read error %d at %d. file='%s'", Read, Pos, pFilename);
+ dbg_msg("sound/opus", "op_read error %d at %d. file='%s'", Read, Pos, pContext);
return CSampleHandle();
}
else if(Read == 0) // EOF
@@ -428,7 +415,6 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
}
op_free(pOpusFile);
- mem_free(pFileData);
pSample->m_Channels = NumChannels;
pSample->m_Rate = 48000;
@@ -438,13 +424,56 @@ ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
pSample->m_PausedAt = 0;
if(m_pConfig->m_Debug)
- dbg_msg("sound/opus", "loaded %s (%d samples, %d channels)", pFilename, TotalSamples, NumChannels);
+ dbg_msg("sound/opus", "loaded %s (%d samples, %d channels)", pContext, TotalSamples, NumChannels);
RateConvert(SampleID);
lock_unlock(m_SoundLock);
return CreateSampleHandle(SampleID);
}
+ISound::CSampleHandle CSound::LoadOpus(const char *pFilename)
+{
+ // don't waste memory on sound when we are stress testing
+#ifdef CONF_DEBUG
+ if(m_pConfig->m_DbgStress)
+ return CSampleHandle();
+#endif
+
+ // no need to load sound when we are running with no sound
+ if(!m_SoundEnabled)
+ return CSampleHandle();
+
+ if(!m_pStorage)
+ return CSampleHandle();
+
+ unsigned char *pFileData;
+ unsigned FileSize;
+ if(!m_pStorage->ReadFile(pFilename, IStorage::TYPE_ALL, (void **) &pFileData, &FileSize))
+ {
+ dbg_msg("sound/opus", "failed to open file. filename='%s'", pFilename);
+ return CSampleHandle();
+ }
+
+ ISound::CSampleHandle Sample = LoadOpusMemory(pFilename, pFileData, FileSize);
+ mem_free(pFileData);
+ return Sample;
+}
+
+bool CSound::UnloadSample(CSampleHandle *pSampleID)
+{
+ if(!pSampleID)
+ return false;
+
+ if(m_aSamples[pSampleID->Id()].m_pData)
+ {
+ mem_free(m_aSamples[pSampleID->Id()].m_pData);
+ m_aSamples[pSampleID->Id()].m_pData = 0;
+ pSampleID->Invalidate();
+ return true;
+ }
+ return false;
+}
+
void CSound::SetListenerPos(float x, float y)
{
m_CenterX = (int) x;
diff --git a/src/engine/client/sound.h b/src/engine/client/sound.h
index 2a3e2bf1..d3d3b0b2 100644
--- a/src/engine/client/sound.h
+++ b/src/engine/client/sound.h
@@ -24,7 +24,9 @@ class CSound : public IEngineSound
virtual bool IsSoundEnabled() { return m_SoundEnabled != 0; }
+ virtual CSampleHandle LoadOpusMemory(const char *pContext, const unsigned char *pData, int DataSize);
virtual CSampleHandle LoadOpus(const char *pFilename);
+ virtual bool UnloadSample(CSampleHandle *pSampleID);
virtual void SetListenerPos(float x, float y);
virtual void SetChannelVolume(int ChannelID, float Vol);
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/engine/sound.h b/src/engine/sound.h
index 7a95f4ea..e4a193e7 100644
--- a/src/engine/sound.h
+++ b/src/engine/sound.h
@@ -28,11 +28,14 @@ class ISound : public IInterface
bool IsValid() const { return Id() >= 0; }
int Id() const { return m_Id; }
+ void Invalidate() { m_Id = -1; }
};
virtual bool IsSoundEnabled() = 0;
+ virtual CSampleHandle LoadOpusMemory(const char *pContext, const unsigned char *pData, int DataSize) = 0;
virtual CSampleHandle LoadOpus(const char *pFilename) = 0;
+ virtual bool UnloadSample(CSampleHandle *pSampleID) = 0;
virtual void SetChannelVolume(int ChannelID, float Volume) = 0;
virtual void SetListenerPos(float x, float y) = 0;
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..27ef14c3
--- /dev/null
+++ b/src/game/client/components/resource.cpp
@@ -0,0 +1,229 @@
+#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::OnRender()
+{
+ 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);
+ }
+ }
+}
+
+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();
+ }
+ }
+}
+
+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..6f5bad1f
--- /dev/null
+++ b/src/game/client/components/resource.h
@@ -0,0 +1,40 @@
+#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;
+ };
+
+ array m_lResources;
+ void RequestDownload(const Uuid *pRequest);
+ bool LoadResource(CClientResource *pResource);
+
+ void RenderImageEntity(const CNetObj_CustomImageEntity *pPrev, const CNetObj_CustomImageEntity *pCur);
+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/components/sounds.cpp b/src/game/client/components/sounds.cpp
index 66cc4bb3..2c83fa2a 100644
--- a/src/game/client/components/sounds.cpp
+++ b/src/game/client/components/sounds.cpp
@@ -128,7 +128,7 @@ void CSounds::OnRender()
int64 Now = time_get();
if(m_QueueWaitTime <= Now)
{
- Play(m_aQueue[0].m_Channel, m_aQueue[0].m_SetId, 1.0f);
+ PlaySample(m_aQueue[0].m_Channel, m_aQueue[0].m_Sample, 1.0f);
m_QueueWaitTime = Now + time_freq() * 3 / 10; // wait 300ms before playing the next one
if(--m_QueuePos > 0)
mem_move(m_aQueue, m_aQueue + 1, m_QueuePos * sizeof(QueueEntry));
@@ -143,7 +143,7 @@ void CSounds::ClearQueue()
m_QueueWaitTime = time_get();
}
-void CSounds::Enqueue(int Channel, int SetId)
+void CSounds::EnqueueSample(int Channel, ISound::CSampleHandle Sample)
{
if(m_pClient->m_SuppressEvents)
return;
@@ -151,45 +151,75 @@ void CSounds::Enqueue(int Channel, int SetId)
return;
if(Channel != CHN_MUSIC && Config()->m_ClEditor)
return;
+ if(!Sample.IsValid())
+ return;
m_aQueue[m_QueuePos].m_Channel = Channel;
- m_aQueue[m_QueuePos++].m_SetId = SetId;
+ m_aQueue[m_QueuePos++].m_Sample = Sample;
}
-void CSounds::Play(int Chn, int SetId, float Vol)
+void CSounds::PlaySample(int Channel, ISound::CSampleHandle Sample, float Vol)
{
if(m_pClient->m_SuppressEvents)
return;
- if(Chn == CHN_MUSIC && !Config()->m_SndMusic)
+ if(Channel == CHN_MUSIC && !Config()->m_SndMusic)
return;
- ISound::CSampleHandle SampleId = GetSampleId(SetId);
- if(!SampleId.IsValid())
+ if(!Sample.IsValid())
return;
int Flags = 0;
- if(Chn == CHN_MUSIC)
+ if(Channel == CHN_MUSIC)
Flags = ISound::FLAG_LOOP;
- Sound()->Play(Chn, SampleId, Flags);
+ Sound()->Play(Channel, Sample, Flags);
}
-void CSounds::PlayAt(int Chn, int SetId, float Vol, vec2 Pos)
+void CSounds::PlaySampleAt(int Channel, ISound::CSampleHandle Sample, float Vol, vec2 Pos)
{
if(m_pClient->m_SuppressEvents)
return;
- if(Chn == CHN_MUSIC && !Config()->m_SndMusic)
+ if(Channel == CHN_MUSIC && !Config()->m_SndMusic)
return;
- ISound::CSampleHandle SampleId = GetSampleId(SetId);
- if(!SampleId.IsValid())
+ if(!Sample.IsValid())
return;
int Flags = 0;
- if(Chn == CHN_MUSIC)
+ if(Channel == CHN_MUSIC)
Flags = ISound::FLAG_LOOP;
- Sound()->PlayAt(Chn, SampleId, Flags, Pos.x, Pos.y);
+ Sound()->PlayAt(Channel, Sample, Flags, Pos.x, Pos.y);
+}
+
+void CSounds::StopSample(ISound::CSampleHandle Sample)
+{
+ if(m_WaitForSoundJob)
+ return;
+
+ Sound()->Stop(Sample);
+}
+
+bool CSounds::IsPlayingSample(ISound::CSampleHandle Sample)
+{
+ if(m_WaitForSoundJob)
+ return false;
+ return Sound()->IsPlaying(Sample);
+}
+
+void CSounds::Enqueue(int Channel, int SetId)
+{
+ EnqueueSample(Channel, GetSampleId(SetId));
+}
+
+void CSounds::Play(int Channel, int SetId, float Vol)
+{
+ PlaySample(Channel, GetSampleId(SetId), Vol);
+}
+
+void CSounds::PlayAt(int Channel, int SetId, float Vol, vec2 Pos)
+{
+ PlaySampleAt(Channel, GetSampleId(SetId), Vol, Pos);
}
void CSounds::Stop(int SetId)
@@ -200,7 +230,7 @@ void CSounds::Stop(int SetId)
CDataSoundset *pSet = &g_pData->m_aSounds[SetId];
for(int i = 0; i < pSet->m_NumSounds; i++)
- Sound()->Stop(pSet->m_aSounds[i].m_Id);
+ StopSample(pSet->m_aSounds[i].m_Id);
}
bool CSounds::IsPlaying(int SetId)
@@ -211,8 +241,18 @@ bool CSounds::IsPlaying(int SetId)
CDataSoundset *pSet = &g_pData->m_aSounds[SetId];
for(int i = 0; i < pSet->m_NumSounds; i++)
{
- if(Sound()->IsPlaying(pSet->m_aSounds[i].m_Id))
+ if(IsPlayingSample(pSet->m_aSounds[i].m_Id))
return true;
}
return false;
}
+
+ISound::CSampleHandle CSounds::LoadSampleMemory(const char *pContext, const unsigned char *pData, int DataSize)
+{
+ return m_pClient->Sound()->LoadOpusMemory(pContext, pData, DataSize);
+}
+
+bool CSounds::UnloadSample(ISound::CSampleHandle *pSample)
+{
+ return Sound()->UnloadSample(pSample);
+}
diff --git a/src/game/client/components/sounds.h b/src/game/client/components/sounds.h
index 0f56593b..b2e7b67d 100644
--- a/src/game/client/components/sounds.h
+++ b/src/game/client/components/sounds.h
@@ -16,7 +16,7 @@ class CSounds : public CComponent
struct QueueEntry
{
int m_Channel;
- int m_SetId;
+ ISound::CSampleHandle m_Sample;
} m_aQueue[QUEUE_SIZE];
int m_QueuePos;
int64 m_QueueWaitTime;
@@ -42,11 +42,20 @@ class CSounds : public CComponent
virtual void OnRender();
void ClearQueue();
+ void EnqueueSample(int Channel, ISound::CSampleHandle Sample);
+ void PlaySample(int Channel, ISound::CSampleHandle Sample, float Vol);
+ void PlaySampleAt(int Channel, ISound::CSampleHandle Sample, float Vol, vec2 Pos);
+ void StopSample(ISound::CSampleHandle Sample);
+ bool IsPlayingSample(ISound::CSampleHandle Sample);
+
void Enqueue(int Channel, int SetId);
void Play(int Channel, int SetId, float Vol);
void PlayAt(int Channel, int SetId, float Vol, vec2 Pos);
void Stop(int SetId);
bool IsPlaying(int SetId);
+
+ ISound::CSampleHandle LoadSampleMemory(const char *pContext, const unsigned char *pData, int DataSize);
+ bool UnloadSample(ISound::CSampleHandle *pSample);
};
#endif
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index 9289cf1a..2558b547 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -46,6 +46,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"
@@ -118,6 +119,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);
@@ -254,6 +256,7 @@ void CGameClient::OnConsoleInit()
m_pMapLayersBackGround = &::gs_MapLayersBackGround;
m_pMapLayersForeGround = &::gs_MapLayersForeGround;
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);
@@ -271,6 +274,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);
@@ -1089,6 +1093,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));
+ }
}
}
@@ -1306,6 +1315,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;
@@ -1441,6 +1461,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 eea47326..f43f14eb 100644
--- a/src/game/client/gameclient.h
+++ b/src/game/client/gameclient.h
@@ -190,6 +190,8 @@ class CGameClient : public IGameClient
// interpolated position
vec2 m_Position;
+ bool m_UseCustomGameTexture;
+ IGraphics::CTextureHandle m_GameTexture;
};
CCharacterInfo m_aCharacters[MAX_CLIENTS];
@@ -345,6 +347,7 @@ class CGameClient : public IGameClient
class CItems *m_pItems;
class CMapLayers *m_pMapLayersBackGround;
class CMapLayers *m_pMapLayersForeGround;
+ 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 3f8da342..686fbae2 100644
--- a/src/game/server/gamecontext.cpp
+++ b/src/game/server/gamecontext.cpp
@@ -184,6 +184,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)
{
@@ -542,6 +557,7 @@ void CGameContext::OnTick()
{
m_apPlayers[i]->Tick();
m_apPlayers[i]->PostTick();
+ m_ResourceManager.TrySendResourceInfo(i);
}
}
@@ -729,6 +745,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)
@@ -1131,6 +1149,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
{
@@ -1600,6 +1623,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 85acaa60..ed80fec7 100644
--- a/src/game/server/gamecontext.h
+++ b/src/game/server/gamecontext.h
@@ -12,6 +12,7 @@
#include "eventhandler.h"
#include "gameworld.h"
+#include "resource.h"
/*
Tick
@@ -92,9 +93,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
@@ -139,6 +142,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 69619b28..5584f6b1 100644
--- a/src/game/server/gamemodes/infection/reinfected.cpp
+++ b/src/game/server/gamemodes/infection/reinfected.cpp
@@ -374,6 +374,11 @@ void CGameControllerReinfected::Tick()
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 79b626cc..e2fca776 100644
--- a/src/game/server/gamemodes/infection/reinfected.h
+++ b/src/game/server/gamemodes/infection/reinfected.h
@@ -37,6 +37,7 @@ class CGameControllerReinfected : public IGameController
virtual int OnCharacterFireWeapon(class CCharacter *pChr, vec2 Direction, int Weapon);
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 c612e177..ef1a775b 100644
--- a/src/game/variables.h
+++ b/src/game/variables.h
@@ -144,6 +144,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")