From 2574433d58bfe9e1be890f6cd8c099d35b6f4729 Mon Sep 17 00:00:00 2001 From: Bamcane Date: Mon, 9 Feb 2026 23:59:41 +0800 Subject: [PATCH 1/5] Basic custom resource --- CMakeLists.txt | 5 + datasrc/datatypes.py | 27 +++ datasrc/network.py | 24 +++ src/base/uuid.h | 10 + src/engine/client/sound.cpp | 77 +++++--- src/engine/client/sound.h | 2 + src/engine/shared/storage.cpp | 1 + src/engine/sound.h | 3 + src/game/client/components/resource.cpp | 186 ++++++++++++++++++ src/game/client/components/resource.h | 37 ++++ src/game/client/components/sounds.cpp | 74 +++++-- src/game/client/components/sounds.h | 11 +- src/game/client/gameclient.cpp | 9 + src/game/client/gameclient.h | 1 + src/game/resource.h | 27 +++ src/game/server/gamecontext.cpp | 23 +++ src/game/server/gamecontext.h | 4 + .../server/gamemodes/infection/reinfected.cpp | 10 + src/game/server/resource.cpp | 149 ++++++++++++++ src/game/server/resource.h | 36 ++++ src/game/variables.h | 2 + 21 files changed, 676 insertions(+), 42 deletions(-) create mode 100644 src/game/client/components/resource.cpp create mode 100644 src/game/client/components/resource.h create mode 100644 src/game/resource.h create mode 100644 src/game/server/resource.cpp create mode 100644 src/game/server/resource.h 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..0367e591 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,6 +279,10 @@ 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)") ]) ] @@ -490,4 +499,19 @@ NetStringStrict("m_Arguments") ]), + NetMessageEx("Sv_CustomRes", "custom-res@netmsg.teeworlds.wiki", [ + NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), + NetEnum("m_Type", Resources), + NetStringStrict("m_Name"), + NetIntAny("m_Crc"), + NetIntRange("m_Size", 0, 'max_int'), + NetRawDataFixed("m_Sha256", "sizeof(SHA256_DIGEST)"), + ]), + NetMessageEx("Sv_CustomResData", "custom-res-data@netmsg.teeworlds.wiki", [ + NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), + NetRawData("m_Data"), + ]), + NetMessageEx("Cl_ReqeustCustomRes", "request-custom-res@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/resource.cpp b/src/game/client/components/resource.cpp new file mode 100644 index 00000000..3eb285ff --- /dev/null +++ b/src/game/client/components/resource.cpp @@ -0,0 +1,186 @@ +#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.res.%d.tmp", pName, *pCrc, aSha256, pid()); + else + str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.res", pName, *pCrc, aSha256); +} + +CClientResManager::CClientResManager() +{ + m_lResources.clear(); +} + +void CClientResManager::RequestDownload(const Uuid *pRequest) +{ + CNetMsg_Cl_ReqeustCustomRes 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::OnMapLoad() +{ + 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); + } + m_lResources.clear(); +} + +void CClientResManager::OnMessage(int MsgType, void *pRawMsg) +{ + if(MsgType == NETMSGTYPE_SV_CUSTOMRES) + { + CNetMsg_Sv_CustomRes *pMsg = (CNetMsg_Sv_CustomRes *) 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; + } + 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(); + 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); + + 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_CUSTOMRESDATA) + { + CNetMsg_Sv_CustomResData *pMsg = (CNetMsg_Sv_CustomResData *) pRawMsg; + Uuid TargetResource = *static_cast(pMsg->m_Uuid); + + CClientResource TargetRes; + TargetRes.m_Uuid = TargetResource; + sorted_array::range r = ::find_binary(m_lResources.all(), TargetRes); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return; + + CClientResource &Resource = r.front(); + Resource.m_DownloadedSize += pMsg->m_DataSize; + if(Resource.m_DownloadedSize > Resource.m_DataSize) + { + io_close(Resource.m_DownloadTemp); + Storage()->RemoveFile(Resource.m_aTempPath, IStorage::TYPE_SAVE); + m_lResources.remove(Resource); + return; // invalid! + } + io_write(Resource.m_DownloadTemp, pMsg->m_Data, pMsg->m_DataSize); + if(Resource.m_DownloadedSize == Resource.m_DataSize) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), Localize("Resource '%s': download complete"), Resource.m_aName); + UI()->DoToast(aBuf); + io_close(Resource.m_DownloadTemp); + + Storage()->RemoveFile(Resource.m_aPath, IStorage::TYPE_SAVE); + Storage()->RenameFile(Resource.m_aTempPath, Resource.m_aPath, IStorage::TYPE_SAVE); + + if(!LoadResource(&Resource)) + { + Storage()->RemoveFile(Resource.m_aPath, IStorage::TYPE_SAVE); + m_lResources.remove(Resource); + return; // invalid! + } + } + else + RequestDownload(&TargetResource); + } +} + +bool CClientResManager::IsResourceSound(Uuid ResID) +{ + CClientResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return false; + return r.front().m_Type == RESOURCE_SOUND; +} + +bool CClientResManager::IsResourceImage(Uuid ResID) +{ + CClientResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return false; + return r.front().m_Type == RESOURCE_IMAGE; +} + +ISound::CSampleHandle CClientResManager::GetResourceSample(Uuid ResID) +{ + CClientResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return ISound::CSampleHandle(); + return r.front().m_Sample; +} + +IGraphics::CTextureHandle CClientResManager::GetResourceTexture(Uuid ResID) +{ + CClientResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return IGraphics::CTextureHandle(); + return r.front().m_Texture; +} diff --git a/src/game/client/components/resource.h b/src/game/client/components/resource.h new file mode 100644 index 00000000..60e5f60c --- /dev/null +++ b/src/game/client/components/resource.h @@ -0,0 +1,37 @@ +#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; + ISound::CSampleHandle m_Sample; + IGraphics::CTextureHandle m_Texture; + IOHANDLE m_DownloadTemp; + }; + + sorted_array m_lResources; + void RequestDownload(const Uuid *pRequest); + bool LoadResource(CClientResource *pResource); +public: + CClientResManager(); + virtual void OnMapLoad(); + virtual void OnMessage(int MsgType, void *pRawMsg); + + bool IsResourceSound(Uuid ResID); + bool IsResourceImage(Uuid ResID); + 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..07214086 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,11 +256,13 @@ 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); m_All.Add(m_pCountryFlags); m_All.Add(m_pMapimages); + m_All.Add(m_pResourceManager); m_All.Add(m_pEffects); // doesn't render anything, just updates effects m_All.Add(m_pParticles); // doesn't render anything, just updates all the particles m_All.Add(m_pBinds); @@ -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)); + } } } diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index eea47326..5654a533 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -345,6 +345,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..16aee9a3 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()->IsResourceSound(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) { @@ -729,6 +744,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 +1148,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_REQEUSTCUSTOMRES) + { + CNetMsg_Cl_ReqeustCustomRes *pMsg = (CNetMsg_Cl_ReqeustCustomRes *) pRawMsg; + ResourceManager()->SendResourceData(ClientID, *static_cast(pMsg->m_Uuid)); + } } else { @@ -1600,6 +1622,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..c5dc8f9c 100644 --- a/src/game/server/gamemodes/infection/reinfected.cpp +++ b/src/game/server/gamemodes/infection/reinfected.cpp @@ -181,6 +181,14 @@ void CGameControllerReinfected::StartRandomInfection() str_format(aBuf, sizeof(aBuf), "'%s' has been infected!", Server()->ClientName(aPlayers[RandomInfected])); GameServer()->SendChat(-1, CHAT_ALL, -1, aBuf); } + Uuid Sound = calculate_uuid("ninja_attack_04"); + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(GameServer()->m_apPlayers[i]) + { + GameServer()->CreateCustomSound(GameServer()->m_apPlayers[i]->m_ViewPos, Sound, CmaskOne(i)); + } + } } } @@ -205,6 +213,8 @@ CGameControllerReinfected::CGameControllerReinfected(CGameContext *pGameServer) { m_pGameType = "Reinfected"; m_pHelper = new CReinfectedHelper(this); + + GameServer()->ResourceManager()->AddResource("audio/wp_ninja_attack-04.opus", "ninja_attack_04", calculate_uuid("ninja_attack_04")); } CGameControllerReinfected::~CGameControllerReinfected() diff --git a/src/game/server/resource.cpp b/src/game/server/resource.cpp new file mode 100644 index 00000000..15d5563a --- /dev/null +++ b/src/game/server/resource.cpp @@ -0,0 +1,149 @@ +#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::CServerResManager() +{ +} + +CServerResManager::~CServerResManager() +{ + Clear(); +} + +void CServerResManager::Init(CGameContext *pGameContext) +{ + m_pGameContext = pGameContext; +} + +void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) +{ + CServerResource Res; + Res.m_Uuid = RequestUuid; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return; + + int ChunkSize = CResource::CHUNK_SIZE; + CServerResource *pTarget = &r.front(); + + // 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_CustomResData Msg; + Msg.m_Uuid = &pTarget->m_Uuid; + Msg.m_Data = &pTarget->m_pData[Offset]; + Msg.m_DataSize = ChunkSize; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, 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; + for(int i = 0; i < m_lResources.size(); i++) + { + CNetMsg_Sv_CustomRes Resource; + Resource.m_Uuid = &m_lResources[i].m_Uuid; + Resource.m_Type = m_lResources[i].m_Type; + Resource.m_Name = m_lResources[i].m_aName; + Resource.m_Crc = m_lResources[i].m_Crc; + Resource.m_Sha256 = &m_lResources[i].m_Sha256; + Resource.m_Size = m_lResources[i].m_DataSize; + Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + + m_lResources[i].m_aDownloadChunks[i] = 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; + } +} + +bool CServerResManager::IsResourceSound(Uuid ResID) +{ + CServerResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return false; + return r.front().m_Type == RESOURCE_SOUND; +} + +bool CServerResManager::IsResourceImage(Uuid ResID) +{ + CServerResource Res; + Res.m_Uuid = ResID; + sorted_array::range r = ::find_binary(m_lResources.all(), Res); + if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + return false; + return r.front().m_Type == RESOURCE_IMAGE; +} diff --git a/src/game/server/resource.h b/src/game/server/resource.h new file mode 100644 index 00000000..77e126de --- /dev/null +++ b/src/game/server/resource.h @@ -0,0 +1,36 @@ +#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]; + }; + + sorted_array m_lResources; +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(); + + bool IsResourceSound(Uuid ResID); + bool IsResourceImage(Uuid ResID); +}; + +#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") From 33e8a97715517e62d3a6f7a89d7b720f7bd5831c Mon Sep 17 00:00:00 2001 From: Bamcane Date: Tue, 10 Feb 2026 01:15:15 +0800 Subject: [PATCH 2/5] Resource send fix --- .../server/gamemodes/infection/reinfected.cpp | 10 ----- src/game/server/resource.cpp | 40 +++++++++---------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/game/server/gamemodes/infection/reinfected.cpp b/src/game/server/gamemodes/infection/reinfected.cpp index c5dc8f9c..69619b28 100644 --- a/src/game/server/gamemodes/infection/reinfected.cpp +++ b/src/game/server/gamemodes/infection/reinfected.cpp @@ -181,14 +181,6 @@ void CGameControllerReinfected::StartRandomInfection() str_format(aBuf, sizeof(aBuf), "'%s' has been infected!", Server()->ClientName(aPlayers[RandomInfected])); GameServer()->SendChat(-1, CHAT_ALL, -1, aBuf); } - Uuid Sound = calculate_uuid("ninja_attack_04"); - for(int i = 0; i < MAX_CLIENTS; i++) - { - if(GameServer()->m_apPlayers[i]) - { - GameServer()->CreateCustomSound(GameServer()->m_apPlayers[i]->m_ViewPos, Sound, CmaskOne(i)); - } - } } } @@ -213,8 +205,6 @@ CGameControllerReinfected::CGameControllerReinfected(CGameContext *pGameServer) { m_pGameType = "Reinfected"; m_pHelper = new CReinfectedHelper(this); - - GameServer()->ResourceManager()->AddResource("audio/wp_ninja_attack-04.opus", "ninja_attack_04", calculate_uuid("ninja_attack_04")); } CGameControllerReinfected::~CGameControllerReinfected() diff --git a/src/game/server/resource.cpp b/src/game/server/resource.cpp index 15d5563a..8ad96720 100644 --- a/src/game/server/resource.cpp +++ b/src/game/server/resource.cpp @@ -51,11 +51,11 @@ void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) else pTarget->m_aDownloadChunks[ClientID]++; - CNetMsg_Sv_CustomResData Msg; - Msg.m_Uuid = &pTarget->m_Uuid; - Msg.m_Data = &pTarget->m_pData[Offset]; - Msg.m_DataSize = ChunkSize; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + CNetMsg_Sv_CustomResData Msg; + Msg.m_Uuid = &pTarget->m_Uuid; + 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) { @@ -101,21 +101,21 @@ void CServerResManager::AddResource(const char *pPath, const char *pName, const void CServerResManager::OnClientEnter(int ClientID) { - if(Server()->GetClientVersion(ClientID) < 0x0706) - return; - for(int i = 0; i < m_lResources.size(); i++) - { - CNetMsg_Sv_CustomRes Resource; - Resource.m_Uuid = &m_lResources[i].m_Uuid; - Resource.m_Type = m_lResources[i].m_Type; - Resource.m_Name = m_lResources[i].m_aName; - Resource.m_Crc = m_lResources[i].m_Crc; - Resource.m_Sha256 = &m_lResources[i].m_Sha256; - Resource.m_Size = m_lResources[i].m_DataSize; - Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); - - m_lResources[i].m_aDownloadChunks[i] = 0; - } + if(Server()->GetClientVersion(ClientID) < 0x0706) + return; + for(int i = 0; i < m_lResources.size(); i++) + { + CNetMsg_Sv_CustomRes Resource; + Resource.m_Uuid = &m_lResources[i].m_Uuid; + Resource.m_Type = m_lResources[i].m_Type; + Resource.m_Name = m_lResources[i].m_aName; + Resource.m_Crc = m_lResources[i].m_Crc; + Resource.m_Sha256 = &m_lResources[i].m_Sha256; + Resource.m_Size = m_lResources[i].m_DataSize; + Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD, ClientID); + + m_lResources[i].m_aDownloadChunks[ClientID] = 0; + } } void CServerResManager::Clear() From 7750b45b3e374406d22cf815399eb9e878ce4fbf Mon Sep 17 00:00:00 2001 From: Bamcane Date: Tue, 10 Feb 2026 19:39:57 +0800 Subject: [PATCH 3/5] Custom Image Entity --- datasrc/network.py | 27 ++++++-- src/game/client/components/resource.cpp | 65 +++++++++++++++++-- src/game/client/components/resource.h | 3 + src/game/client/gameclient.cpp | 2 +- src/game/server/gamecontext.cpp | 4 +- .../server/gamemodes/infection/reinfected.cpp | 22 +++++++ .../server/gamemodes/infection/reinfected.h | 1 + src/game/server/resource.cpp | 4 +- 8 files changed, 111 insertions(+), 17 deletions(-) diff --git a/datasrc/network.py b/datasrc/network.py index 0367e591..ef0768d1 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -282,8 +282,25 @@ ]), NetEventEx("CustomSoundWorld:Common", "custom-sound-world@netevent.teeworlds.wiki", [ - NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)") - ]) + 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"), + ]), ] Messages = [ @@ -499,7 +516,7 @@ NetStringStrict("m_Arguments") ]), - NetMessageEx("Sv_CustomRes", "custom-res@netmsg.teeworlds.wiki", [ + NetMessageEx("Sv_CustomResource", "custom-resource@netmsg.teeworlds.wiki", [ NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), NetEnum("m_Type", Resources), NetStringStrict("m_Name"), @@ -507,11 +524,11 @@ NetIntRange("m_Size", 0, 'max_int'), NetRawDataFixed("m_Sha256", "sizeof(SHA256_DIGEST)"), ]), - NetMessageEx("Sv_CustomResData", "custom-res-data@netmsg.teeworlds.wiki", [ + NetMessageEx("Sv_CustomResourceData", "custom-resource-data@netmsg.teeworlds.wiki", [ NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), NetRawData("m_Data"), ]), - NetMessageEx("Cl_ReqeustCustomRes", "request-custom-res@netmsg.teeworlds.wiki", [ + NetMessageEx("Cl_ReqeustCustomResource", "request-custom-resource@netmsg.teeworlds.wiki", [ NetRawDataFixed("m_Uuid", "sizeof(Uuid)"), ]), ] diff --git a/src/game/client/components/resource.cpp b/src/game/client/components/resource.cpp index 3eb285ff..3fa3b1c7 100644 --- a/src/game/client/components/resource.cpp +++ b/src/game/client/components/resource.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "resource.h" #include "sounds.h" @@ -9,9 +10,9 @@ static void FormatResourcePath(char *pBuffer, int BufferSize, const char *pName, char aSha256[SHA256_MAXSTRSIZE]; sha256_str(*pSha256, aSha256, sizeof(aSha256)); if(Temp) - str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.res.%d.tmp", pName, *pCrc, aSha256, pid()); + str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.twres.%d.tmp", pName, *pCrc, aSha256, pid()); else - str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.res", pName, *pCrc, aSha256); + str_format(pBuffer, BufferSize, "downloadedres/%s_%08x%s.twres", pName, *pCrc, aSha256); } CClientResManager::CClientResManager() @@ -21,7 +22,10 @@ CClientResManager::CClientResManager() void CClientResManager::RequestDownload(const Uuid *pRequest) { - CNetMsg_Cl_ReqeustCustomRes Msg; + if(!pRequest) + return; + + CNetMsg_Cl_ReqeustCustomResource Msg; Msg.m_Uuid = pRequest; Client()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD); } @@ -47,6 +51,25 @@ bool CClientResManager::LoadResource(CClientResource *pResource) 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::OnMapLoad() { for(int i = 0; i < m_lResources.size(); i++) @@ -59,11 +82,28 @@ void CClientResManager::OnMapLoad() m_lResources.clear(); } +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_CUSTOMRES) + if(MsgType == NETMSGTYPE_SV_CUSTOMRESOURCE) { - CNetMsg_Sv_CustomRes *pMsg = (CNetMsg_Sv_CustomRes *) pRawMsg; + CNetMsg_Sv_CustomResource *pMsg = (CNetMsg_Sv_CustomResource *) pRawMsg; // protect the player from nasty map names for(int i = 0; pMsg->m_Name[i]; i++) @@ -82,6 +122,16 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "resource", "invalid resource size"); return; } + { + CClientResource TargetRes; + TargetRes.m_Uuid = *static_cast(pMsg->m_Uuid); + sorted_array::range r = ::find_binary(m_lResources.all(), TargetRes); + if(!r.empty()) // 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; @@ -91,6 +141,7 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) Resource.m_DownloadedSize = 0; Resource.m_Sample = ISound::CSampleHandle(); Resource.m_Texture = IGraphics::CTextureHandle(); + Resource.m_Type = pMsg->m_Type; 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); @@ -102,9 +153,9 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) RequestDownload(&m_lResources[Index].m_Uuid); } } - else if(MsgType == NETMSGTYPE_SV_CUSTOMRESDATA) + else if(MsgType == NETMSGTYPE_SV_CUSTOMRESOURCEDATA) { - CNetMsg_Sv_CustomResData *pMsg = (CNetMsg_Sv_CustomResData *) pRawMsg; + CNetMsg_Sv_CustomResourceData *pMsg = (CNetMsg_Sv_CustomResourceData *) pRawMsg; Uuid TargetResource = *static_cast(pMsg->m_Uuid); CClientResource TargetRes; diff --git a/src/game/client/components/resource.h b/src/game/client/components/resource.h index 60e5f60c..8e48f84f 100644 --- a/src/game/client/components/resource.h +++ b/src/game/client/components/resource.h @@ -23,9 +23,12 @@ class CClientResManager : public CComponent sorted_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 OnMapLoad(); + virtual void OnRender(); virtual void OnMessage(int MsgType, void *pRawMsg); bool IsResourceSound(Uuid ResID); diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 07214086..cd046ead 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -262,7 +262,6 @@ void CGameClient::OnConsoleInit() m_All.Add(m_pSkins); m_All.Add(m_pCountryFlags); m_All.Add(m_pMapimages); - m_All.Add(m_pResourceManager); m_All.Add(m_pEffects); // doesn't render anything, just updates effects m_All.Add(m_pParticles); // doesn't render anything, just updates all the particles m_All.Add(m_pBinds); @@ -275,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); diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 16aee9a3..e666a756 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -1148,9 +1148,9 @@ 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_REQEUSTCUSTOMRES) + else if(MsgID == NETMSGTYPE_CL_REQEUSTCUSTOMRESOURCE) { - CNetMsg_Cl_ReqeustCustomRes *pMsg = (CNetMsg_Cl_ReqeustCustomRes *) pRawMsg; + CNetMsg_Cl_ReqeustCustomResource *pMsg = (CNetMsg_Cl_ReqeustCustomResource *) pRawMsg; ResourceManager()->SendResourceData(ClientID, *static_cast(pMsg->m_Uuid)); } } diff --git a/src/game/server/gamemodes/infection/reinfected.cpp b/src/game/server/gamemodes/infection/reinfected.cpp index 69619b28..41e0aa77 100644 --- a/src/game/server/gamemodes/infection/reinfected.cpp +++ b/src/game/server/gamemodes/infection/reinfected.cpp @@ -205,6 +205,8 @@ CGameControllerReinfected::CGameControllerReinfected(CGameContext *pGameServer) { m_pGameType = "Reinfected"; m_pHelper = new CReinfectedHelper(this); + + GameServer()->ResourceManager()->AddResource("mapres/sun.png", "sun", calculate_uuid("sun")); } CGameControllerReinfected::~CGameControllerReinfected() @@ -374,6 +376,26 @@ void CGameControllerReinfected::Tick() DoWincheckMatch(); } +void CGameControllerReinfected::Snap(int SnappingClient) +{ + IGameController::Snap(SnappingClient); + + CCharacter *pChr = GameServer()->GetPlayerChar(SnappingClient); + if(!pChr) + return; + + CNetObj_CustomImageEntity *pEntity = static_cast(Server()->SnapNewItem(NETOBJTYPE_CUSTOMIMAGEENTITY, 0, sizeof(CNetObj_CustomImageEntity))); + if(!pEntity) + return; + Uuid UuidSun = calculate_uuid("sun"); + mem_copy(pEntity->m_Uuid, &UuidSun, sizeof(pEntity->m_Uuid)); + pEntity->m_X = round_to_int(pChr->GetPos().x); + pEntity->m_Y = round_to_int(pChr->GetPos().y) - 64; + pEntity->m_Width = 64; + pEntity->m_Height = 64; + pEntity->m_Angle = round_to_int(fmod(Server()->Tick() / 50.0f, pi * 2) * 256.0f); +} + 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 index 8ad96720..005902a5 100644 --- a/src/game/server/resource.cpp +++ b/src/game/server/resource.cpp @@ -51,7 +51,7 @@ void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) else pTarget->m_aDownloadChunks[ClientID]++; - CNetMsg_Sv_CustomResData Msg; + CNetMsg_Sv_CustomResourceData Msg; Msg.m_Uuid = &pTarget->m_Uuid; Msg.m_Data = &pTarget->m_pData[Offset]; Msg.m_DataSize = ChunkSize; @@ -105,7 +105,7 @@ void CServerResManager::OnClientEnter(int ClientID) return; for(int i = 0; i < m_lResources.size(); i++) { - CNetMsg_Sv_CustomRes Resource; + CNetMsg_Sv_CustomResource Resource; Resource.m_Uuid = &m_lResources[i].m_Uuid; Resource.m_Type = m_lResources[i].m_Type; Resource.m_Name = m_lResources[i].m_aName; From 7f36b7770b63c18a76009f1ed796eea5721274b5 Mon Sep 17 00:00:00 2001 From: Bamcane Date: Tue, 10 Feb 2026 20:53:19 +0800 Subject: [PATCH 4/5] Custom game texture per character --- datasrc/network.py | 6 +++++ src/game/client/components/players.cpp | 4 +-- src/game/client/components/resource.cpp | 26 ++++++++++++++++++- src/game/client/components/resource.h | 2 ++ src/game/client/gameclient.cpp | 13 ++++++++++ src/game/client/gameclient.h | 2 ++ .../server/gamemodes/infection/reinfected.cpp | 17 ------------ src/game/server/resource.cpp | 3 +++ src/game/server/resource.h | 1 + 9 files changed, 54 insertions(+), 20 deletions(-) diff --git a/datasrc/network.py b/datasrc/network.py index ef0768d1..e4c99b2f 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -301,6 +301,10 @@ NetIntAny("m_Vol"), NetIntAny("m_Offset"), ]), + + NetObjectEx("CharacterGameTexture", "character-game-texture@netobj.teeworlds.wiki", [ + NetRawDataFixedSnapshot("m_Uuid", "sizeof(Uuid)"), + ]), ] Messages = [ @@ -521,11 +525,13 @@ 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", [ 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 index 3fa3b1c7..fb98f445 100644 --- a/src/game/client/components/resource.cpp +++ b/src/game/client/components/resource.cpp @@ -142,6 +142,7 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) 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); @@ -191,11 +192,34 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) return; // invalid! } } - else + else if((pMsg->m_ChunkIndex + 1) % Resource.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(); + } + } +} + bool CClientResManager::IsResourceSound(Uuid ResID) { CClientResource Res; diff --git a/src/game/client/components/resource.h b/src/game/client/components/resource.h index 8e48f84f..4e5907c2 100644 --- a/src/game/client/components/resource.h +++ b/src/game/client/components/resource.h @@ -15,6 +15,7 @@ class CClientResManager : public CComponent 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; @@ -30,6 +31,7 @@ class CClientResManager : public CComponent virtual void OnMapLoad(); virtual void OnRender(); virtual void OnMessage(int MsgType, void *pRawMsg); + virtual void OnStateChange(int NewState, int OldState); bool IsResourceSound(Uuid ResID); bool IsResourceImage(Uuid ResID); diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index cd046ead..2558b547 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -1315,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; @@ -1450,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 5654a533..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]; diff --git a/src/game/server/gamemodes/infection/reinfected.cpp b/src/game/server/gamemodes/infection/reinfected.cpp index 41e0aa77..5584f6b1 100644 --- a/src/game/server/gamemodes/infection/reinfected.cpp +++ b/src/game/server/gamemodes/infection/reinfected.cpp @@ -205,8 +205,6 @@ CGameControllerReinfected::CGameControllerReinfected(CGameContext *pGameServer) { m_pGameType = "Reinfected"; m_pHelper = new CReinfectedHelper(this); - - GameServer()->ResourceManager()->AddResource("mapres/sun.png", "sun", calculate_uuid("sun")); } CGameControllerReinfected::~CGameControllerReinfected() @@ -379,21 +377,6 @@ void CGameControllerReinfected::Tick() void CGameControllerReinfected::Snap(int SnappingClient) { IGameController::Snap(SnappingClient); - - CCharacter *pChr = GameServer()->GetPlayerChar(SnappingClient); - if(!pChr) - return; - - CNetObj_CustomImageEntity *pEntity = static_cast(Server()->SnapNewItem(NETOBJTYPE_CUSTOMIMAGEENTITY, 0, sizeof(CNetObj_CustomImageEntity))); - if(!pEntity) - return; - Uuid UuidSun = calculate_uuid("sun"); - mem_copy(pEntity->m_Uuid, &UuidSun, sizeof(pEntity->m_Uuid)); - pEntity->m_X = round_to_int(pChr->GetPos().x); - pEntity->m_Y = round_to_int(pChr->GetPos().y) - 64; - pEntity->m_Width = 64; - pEntity->m_Height = 64; - pEntity->m_Angle = round_to_int(fmod(Server()->Tick() / 50.0f, pi * 2) * 256.0f); } bool CGameControllerReinfected::DoWincheckMatch() diff --git a/src/game/server/resource.cpp b/src/game/server/resource.cpp index 005902a5..adb7e82c 100644 --- a/src/game/server/resource.cpp +++ b/src/game/server/resource.cpp @@ -23,6 +23,7 @@ CServerResManager::~CServerResManager() void CServerResManager::Init(CGameContext *pGameContext) { m_pGameContext = pGameContext; + m_ChunksPerRequest = Config()->m_SvResDownloadSpeed; } void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) @@ -53,6 +54,7 @@ void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) 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); @@ -112,6 +114,7 @@ void CServerResManager::OnClientEnter(int ClientID) Resource.m_Crc = m_lResources[i].m_Crc; Resource.m_Sha256 = &m_lResources[i].m_Sha256; Resource.m_Size = m_lResources[i].m_DataSize; + Resource.m_ChunkPerRequest = m_ChunksPerRequest; Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD, ClientID); m_lResources[i].m_aDownloadChunks[ClientID] = 0; diff --git a/src/game/server/resource.h b/src/game/server/resource.h index 77e126de..dce5b5b8 100644 --- a/src/game/server/resource.h +++ b/src/game/server/resource.h @@ -19,6 +19,7 @@ class CServerResManager }; sorted_array m_lResources; + int m_ChunksPerRequest; public: CServerResManager(); ~CServerResManager(); From 1c0a8983bea71316c44bb422825c498162fd782d Mon Sep 17 00:00:00 2001 From: Bamcane Date: Tue, 10 Feb 2026 22:49:43 +0800 Subject: [PATCH 5/5] Resource download fix. --- src/game/client/components/resource.cpp | 110 +++++++++--------------- src/game/client/components/resource.h | 8 +- src/game/server/gamecontext.cpp | 3 +- src/game/server/resource.cpp | 73 ++++++++-------- src/game/server/resource.h | 11 +-- 5 files changed, 86 insertions(+), 119 deletions(-) diff --git a/src/game/client/components/resource.cpp b/src/game/client/components/resource.cpp index fb98f445..27ef14c3 100644 --- a/src/game/client/components/resource.cpp +++ b/src/game/client/components/resource.cpp @@ -70,18 +70,6 @@ void CClientResManager::RenderImageEntity(const CNetObj_CustomImageEntity *pPrev Graphics()->QuadsEnd(); } -void CClientResManager::OnMapLoad() -{ - 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); - } - m_lResources.clear(); -} - void CClientResManager::OnRender() { int Num = Client()->SnapNumItems(IClient::SNAP_CURRENT); @@ -122,15 +110,11 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) 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. { - CClientResource TargetRes; - TargetRes.m_Uuid = *static_cast(pMsg->m_Uuid); - sorted_array::range r = ::find_binary(m_lResources.all(), TargetRes); - if(!r.empty()) // 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; - } + 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)); @@ -146,6 +130,7 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) 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])) @@ -158,41 +143,38 @@ void CClientResManager::OnMessage(int MsgType, void *pRawMsg) { CNetMsg_Sv_CustomResourceData *pMsg = (CNetMsg_Sv_CustomResourceData *) pRawMsg; Uuid TargetResource = *static_cast(pMsg->m_Uuid); - - CClientResource TargetRes; - TargetRes.m_Uuid = TargetResource; - sorted_array::range r = ::find_binary(m_lResources.all(), TargetRes); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. + CClientResource *pResource = FindResource(TargetResource); + if(!pResource) return; - - CClientResource &Resource = r.front(); - Resource.m_DownloadedSize += pMsg->m_DataSize; - if(Resource.m_DownloadedSize > Resource.m_DataSize) + pResource->m_DownloadedSize += pMsg->m_DataSize; + if(pResource->m_DownloadedSize > pResource->m_DataSize) { - io_close(Resource.m_DownloadTemp); - Storage()->RemoveFile(Resource.m_aTempPath, IStorage::TYPE_SAVE); - m_lResources.remove(Resource); + 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(Resource.m_DownloadTemp, pMsg->m_Data, pMsg->m_DataSize); - if(Resource.m_DownloadedSize == Resource.m_DataSize) + 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"), Resource.m_aName); + str_format(aBuf, sizeof(aBuf), Localize("Resource '%s': download complete"), pResource->m_aName); UI()->DoToast(aBuf); - io_close(Resource.m_DownloadTemp); + io_close(pResource->m_DownloadTemp); + pResource->m_DownloadTemp = 0; - Storage()->RemoveFile(Resource.m_aPath, IStorage::TYPE_SAVE); - Storage()->RenameFile(Resource.m_aTempPath, Resource.m_aPath, IStorage::TYPE_SAVE); + Storage()->RemoveFile(pResource->m_aPath, IStorage::TYPE_SAVE); + Storage()->RenameFile(pResource->m_aTempPath, pResource->m_aPath, IStorage::TYPE_SAVE); - if(!LoadResource(&Resource)) + if(!LoadResource(pResource)) { - Storage()->RemoveFile(Resource.m_aPath, IStorage::TYPE_SAVE); - m_lResources.remove(Resource); + Storage()->RemoveFile(pResource->m_aPath, IStorage::TYPE_SAVE); + m_lResources.remove(*pResource); return; // invalid! } } - else if((pMsg->m_ChunkIndex + 1) % Resource.m_ChunkPerRequest == 0) + else if((pMsg->m_ChunkIndex + 1) % pResource->m_ChunkPerRequest == 0) RequestDownload(&TargetResource); } } @@ -220,42 +202,28 @@ void CClientResManager::OnStateChange(int NewState, int OldState) } } -bool CClientResManager::IsResourceSound(Uuid ResID) +CClientResManager::CClientResource *CClientResManager::FindResource(Uuid ResourceID) { - CClientResource Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return false; - return r.front().m_Type == RESOURCE_SOUND; -} - -bool CClientResManager::IsResourceImage(Uuid ResID) -{ - CClientResource Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return false; - return r.front().m_Type == RESOURCE_IMAGE; + 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 Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return ISound::CSampleHandle(); - return r.front().m_Sample; + CClientResource *pResource = FindResource(ResID); + if(pResource) + return pResource->m_Sample; + return ISound::CSampleHandle(); } IGraphics::CTextureHandle CClientResManager::GetResourceTexture(Uuid ResID) { - CClientResource Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return IGraphics::CTextureHandle(); - return r.front().m_Texture; + 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 index 4e5907c2..6f5bad1f 100644 --- a/src/game/client/components/resource.h +++ b/src/game/client/components/resource.h @@ -1,7 +1,7 @@ #ifndef GAME_CLIENT_COMPONENTS_RESOURCE_H #define GAME_CLIENT_COMPONENTS_RESOURCE_H -#include +#include #include #include #include @@ -21,20 +21,18 @@ class CClientResManager : public CComponent IOHANDLE m_DownloadTemp; }; - sorted_array m_lResources; + 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 OnMapLoad(); virtual void OnRender(); virtual void OnMessage(int MsgType, void *pRawMsg); virtual void OnStateChange(int NewState, int OldState); + CClientResource *FindResource(Uuid ResourceID); - bool IsResourceSound(Uuid ResID); - bool IsResourceImage(Uuid ResID); ISound::CSampleHandle GetResourceSample(Uuid ResID); IGraphics::CTextureHandle GetResourceTexture(Uuid ResID); }; diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index e666a756..686fbae2 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -186,7 +186,7 @@ void CGameContext::CreateSound(vec2 Pos, int Sound, int64 Mask) void CGameContext::CreateCustomSound(vec2 Pos, Uuid Sound, int64 Mask) { - if(!ResourceManager()->IsResourceSound(Sound)) + if(!ResourceManager()->FindResource(Sound)) return; // create a sound @@ -557,6 +557,7 @@ void CGameContext::OnTick() { m_apPlayers[i]->Tick(); m_apPlayers[i]->PostTick(); + m_ResourceManager.TrySendResourceInfo(i); } } diff --git a/src/game/server/resource.cpp b/src/game/server/resource.cpp index adb7e82c..dd603e9d 100644 --- a/src/game/server/resource.cpp +++ b/src/game/server/resource.cpp @@ -11,6 +11,16 @@ 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() { } @@ -28,14 +38,10 @@ void CServerResManager::Init(CGameContext *pGameContext) void CServerResManager::SendResourceData(int ClientID, const Uuid RequestUuid) { - CServerResource Res; - Res.m_Uuid = RequestUuid; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return; - int ChunkSize = CResource::CHUNK_SIZE; - CServerResource *pTarget = &r.front(); + 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) @@ -105,20 +111,7 @@ void CServerResManager::OnClientEnter(int ClientID) { if(Server()->GetClientVersion(ClientID) < 0x0706) return; - for(int i = 0; i < m_lResources.size(); i++) - { - CNetMsg_Sv_CustomResource Resource; - Resource.m_Uuid = &m_lResources[i].m_Uuid; - Resource.m_Type = m_lResources[i].m_Type; - Resource.m_Name = m_lResources[i].m_aName; - Resource.m_Crc = m_lResources[i].m_Crc; - Resource.m_Sha256 = &m_lResources[i].m_Sha256; - Resource.m_Size = m_lResources[i].m_DataSize; - Resource.m_ChunkPerRequest = m_ChunksPerRequest; - Server()->SendPackMsg(&Resource, MSGFLAG_VITAL | MSGFLAG_FLUSH | MSGFLAG_NORECORD, ClientID); - - m_lResources[i].m_aDownloadChunks[ClientID] = 0; - } + m_aResourceSendIndex[ClientID] = 0; } void CServerResManager::Clear() @@ -129,24 +122,30 @@ void CServerResManager::Clear() 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; + } } -bool CServerResManager::IsResourceSound(Uuid ResID) +void CServerResManager::TrySendResourceInfo(int ClientID) { - CServerResource Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return false; - return r.front().m_Type == RESOURCE_SOUND; -} + if(!GameServer()->m_apPlayers[ClientID]) + return; -bool CServerResManager::IsResourceImage(Uuid ResID) -{ - CServerResource Res; - Res.m_Uuid = ResID; - sorted_array::range r = ::find_binary(m_lResources.all(), Res); - if(r.empty() || r.size() > 1) // there couldn't be uuid collision, if that happened, then the server-side resource name must be wrong. - return false; - return r.front().m_Type == RESOURCE_IMAGE; + 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 index dce5b5b8..86f72275 100644 --- a/src/game/server/resource.h +++ b/src/game/server/resource.h @@ -1,7 +1,7 @@ #ifndef GAME_SERVER_RESOURCE_H #define GAME_SERVER_RESOURCE_H -#include +#include #include class CServerResManager @@ -18,8 +18,10 @@ class CServerResManager int m_aDownloadChunks[MAX_CLIENTS]; }; - sorted_array m_lResources; + int m_aResourceSendIndex[MAX_CLIENTS]; + array m_lResources; int m_ChunksPerRequest; + public: CServerResManager(); ~CServerResManager(); @@ -29,9 +31,8 @@ class CServerResManager void SendResourceData(int ClientID, const Uuid RequestUuid); void OnClientEnter(int ClientID); void Clear(); - - bool IsResourceSound(Uuid ResID); - bool IsResourceImage(Uuid ResID); + void TrySendResourceInfo(int ClientID); + CServerResource *FindResource(Uuid ResourceID); }; #endif // GAME_SERVER_RESOURCE_H \ No newline at end of file