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")