forked from Mirrors/openclonk
1727 lines
49 KiB
C++
1727 lines
49 KiB
C++
/*
|
|
* OpenClonk, http://www.openclonk.org
|
|
*
|
|
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
|
|
* Copyright (c) 2009-2016, The OpenClonk Team and contributors
|
|
*
|
|
* Distributed under the terms of the ISC license; see accompanying file
|
|
* "COPYING" for details.
|
|
*
|
|
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
|
|
* See accompanying file "TRADEMARK" for details.
|
|
*
|
|
* To redistribute this file separately, substitute the full license texts
|
|
* for the above references.
|
|
*/
|
|
#include "C4Include.h"
|
|
#include "network/C4Network2Res.h"
|
|
|
|
#include "c4group/C4Components.h"
|
|
#include "c4group/C4Group.h"
|
|
#include "control/C4GameControl.h"
|
|
#include "lib/C4Random.h"
|
|
#include "game/C4Application.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#endif
|
|
|
|
#ifdef _MSC_VER
|
|
#define snprintf _snprintf
|
|
#pragma warning (disable : 4355)
|
|
#endif
|
|
|
|
// compile debug options
|
|
// #define C4NET2RES_LOAD_ALL
|
|
#define C4NET2RES_DEBUG_LOG
|
|
|
|
// helper
|
|
|
|
class DirSizeHelper
|
|
{
|
|
static uint32_t iSize, iMaxSize;
|
|
static bool Helper(const char *szPath)
|
|
{
|
|
if (szPath[SLen(szPath)-1] == '.')
|
|
return false;
|
|
if (iSize > iMaxSize)
|
|
return false;
|
|
if (DirectoryExists(szPath))
|
|
ForEachFile(szPath, &Helper);
|
|
else if (FileExists(szPath))
|
|
iSize += FileSize(szPath);
|
|
return true;
|
|
}
|
|
public:
|
|
static bool GetDirSize(const char *szPath, uint32_t *pSize, uint32_t inMaxSize = ~0)
|
|
{
|
|
// Security
|
|
if (!pSize) return false;
|
|
// Fold it
|
|
iSize = 0; iMaxSize = inMaxSize;
|
|
ForEachFile(szPath, &Helper);
|
|
// Return
|
|
*pSize = iSize;
|
|
return true;
|
|
}
|
|
};
|
|
uint32_t DirSizeHelper::iSize, DirSizeHelper::iMaxSize;
|
|
|
|
// *** C4Network2ResCore
|
|
|
|
C4Network2ResCore::C4Network2ResCore()
|
|
: iFileSize(~0u), iFileCRC(~0u), iContentsCRC(~0u),
|
|
iChunkSize(C4NetResChunkSize)
|
|
{
|
|
}
|
|
|
|
void C4Network2ResCore::Set(C4Network2ResType enType, int32_t iResID, const char *strFileName, uint32_t inContentsCRC)
|
|
{
|
|
// Initialize base data
|
|
eType = enType; iID = iResID; iDerID = -1;
|
|
fLoadable = false;
|
|
iFileSize = iFileCRC = ~0; iContentsCRC = inContentsCRC;
|
|
iChunkSize = C4NetResChunkSize;
|
|
FileName.Copy(strFileName);
|
|
}
|
|
|
|
void C4Network2ResCore::SetLoadable(uint32_t iSize, uint32_t iCRC)
|
|
{
|
|
fLoadable = true;
|
|
iFileSize = iSize;
|
|
iFileCRC = iCRC;
|
|
}
|
|
|
|
void C4Network2ResCore::Clear()
|
|
{
|
|
eType = NRT_Null;
|
|
iID = iDerID = -1;
|
|
fLoadable = false;
|
|
FileName.Clear();
|
|
iFileSize = iFileCRC = iContentsCRC = ~0;
|
|
fHasFileSHA = false;
|
|
}
|
|
|
|
// C4PacketBase virtuals
|
|
|
|
void C4Network2ResCore::CompileFunc(StdCompiler *pComp)
|
|
{
|
|
pComp->Value(mkNamingAdapt(mkEnumAdaptT<uint8_t>(eType, C4Network2ResType_EnumMap), "Type", NRT_Null));
|
|
pComp->Value(mkNamingAdapt(iID, "ID", -1));
|
|
pComp->Value(mkNamingAdapt(iDerID, "DerID", -1));
|
|
pComp->Value(mkNamingAdapt(fLoadable, "Loadable", true));
|
|
if (fLoadable)
|
|
{
|
|
pComp->Value(mkNamingAdapt(iFileSize, "FileSize", 0U));
|
|
pComp->Value(mkNamingAdapt(iFileCRC, "FileCRC", 0U));
|
|
pComp->Value(mkNamingAdapt(iChunkSize, "ChunkSize", C4NetResChunkSize));
|
|
if (!iChunkSize) pComp->excCorrupt("zero chunk size");
|
|
}
|
|
pComp->Value(mkNamingAdapt(iContentsCRC, "ContentsCRC", 0U));
|
|
pComp->Value(mkNamingCountAdapt(fHasFileSHA, "FileSHA"));
|
|
if (fHasFileSHA)
|
|
pComp->Value(mkNamingAdapt(mkHexAdapt(FileSHA), "FileSHA"));
|
|
pComp->Value(mkNamingAdapt(mkNetFilenameAdapt(FileName), "Filename", ""));
|
|
}
|
|
|
|
// *** C4Network2ResLoad
|
|
|
|
C4Network2ResLoad::C4Network2ResLoad(int32_t inChunk, int32_t inByClient)
|
|
: iChunk(inChunk), Timestamp(time(nullptr)), iByClient(inByClient), pNext(nullptr)
|
|
{
|
|
|
|
}
|
|
|
|
C4Network2ResLoad::~C4Network2ResLoad() = default;
|
|
|
|
bool C4Network2ResLoad::CheckTimeout()
|
|
{
|
|
return difftime(time(nullptr), Timestamp) >= C4NetResLoadTimeout;
|
|
}
|
|
|
|
// *** C4Network2ResChunkData
|
|
|
|
C4Network2ResChunkData::C4Network2ResChunkData() = default;
|
|
|
|
C4Network2ResChunkData::C4Network2ResChunkData(const C4Network2ResChunkData &Data2)
|
|
: C4PacketBase(Data2),
|
|
iChunkCnt(Data2.getChunkCnt())
|
|
{
|
|
// add ranges
|
|
Merge(Data2);
|
|
}
|
|
|
|
C4Network2ResChunkData::~C4Network2ResChunkData()
|
|
{
|
|
Clear();
|
|
}
|
|
|
|
C4Network2ResChunkData &C4Network2ResChunkData::operator =(const C4Network2ResChunkData &Data2)
|
|
{
|
|
// clear, merge
|
|
SetIncomplete(Data2.getChunkCnt());
|
|
Merge(Data2);
|
|
return *this;
|
|
}
|
|
|
|
void C4Network2ResChunkData::SetIncomplete(int32_t inChunkCnt)
|
|
{
|
|
Clear();
|
|
// just set total chunk count
|
|
iChunkCnt = inChunkCnt;
|
|
}
|
|
|
|
void C4Network2ResChunkData::SetComplete(int32_t inChunkCnt)
|
|
{
|
|
Clear();
|
|
// set total chunk count
|
|
iPresentChunkCnt = iChunkCnt = inChunkCnt;
|
|
// create one range
|
|
ChunkRange *pRange = new ChunkRange;
|
|
pRange->Start = 0; pRange->Length = iChunkCnt;
|
|
pRange->Next = nullptr;
|
|
pChunkRanges = pRange;
|
|
}
|
|
|
|
void C4Network2ResChunkData::AddChunk(int32_t iChunk)
|
|
{
|
|
AddChunkRange(iChunk, 1);
|
|
}
|
|
|
|
void C4Network2ResChunkData::AddChunkRange(int32_t iStart, int32_t iLength)
|
|
{
|
|
// security
|
|
if (iStart < 0 || iStart + iLength > iChunkCnt || iLength <= 0) return;
|
|
// find position
|
|
ChunkRange *pRange, *pPrev;
|
|
for (pRange = pChunkRanges, pPrev = nullptr; pRange; pPrev = pRange, pRange = pRange->Next)
|
|
if (pRange->Start >= iStart)
|
|
break;
|
|
// create new
|
|
ChunkRange *pNew = new ChunkRange;
|
|
pNew->Start = iStart; pNew->Length = iLength;
|
|
// add to list
|
|
pNew->Next = pRange;
|
|
(pPrev ? pPrev->Next : pChunkRanges) = pNew;
|
|
// counts
|
|
iPresentChunkCnt += iLength; iChunkRangeCnt++;
|
|
// check merges
|
|
if (pPrev && MergeRanges(pPrev))
|
|
while (MergeRanges(pPrev)) {}
|
|
else
|
|
while (MergeRanges(pNew)) {}
|
|
}
|
|
|
|
void C4Network2ResChunkData::Merge(const C4Network2ResChunkData &Data2)
|
|
{
|
|
// must have same basis chunk count
|
|
assert(iChunkCnt == Data2.getChunkCnt());
|
|
// add ranges
|
|
for (ChunkRange *pRange = Data2.pChunkRanges; pRange; pRange = pRange->Next)
|
|
AddChunkRange(pRange->Start, pRange->Length);
|
|
}
|
|
|
|
void C4Network2ResChunkData::Clear()
|
|
{
|
|
iChunkCnt = iPresentChunkCnt = iChunkRangeCnt = 0;
|
|
// remove all ranges
|
|
while (pChunkRanges)
|
|
{
|
|
ChunkRange *pDelete = pChunkRanges;
|
|
pChunkRanges = pDelete->Next;
|
|
delete pDelete;
|
|
}
|
|
}
|
|
|
|
int32_t C4Network2ResChunkData::GetChunkToRetrieve(const C4Network2ResChunkData &Available, int32_t iLoadingCnt, int32_t *pLoading) const
|
|
{
|
|
// (this version is highly calculation-intensitive, yet the most satisfactory
|
|
// solution I could find)
|
|
|
|
// find everything that should not be retrieved
|
|
C4Network2ResChunkData ChData; Available.GetNegative(ChData);
|
|
ChData.Merge(*this);
|
|
for (int32_t i = 0; i < iLoadingCnt; i++)
|
|
ChData.AddChunk(pLoading[i]);
|
|
// nothing to retrieve?
|
|
if (ChData.isComplete()) return -1;
|
|
// invert to get everything that should be retrieved
|
|
C4Network2ResChunkData ChData2; ChData.GetNegative(ChData2);
|
|
// select chunk (random)
|
|
int32_t iRetrieveChunk = UnsyncedRandom(ChData2.getPresentChunkCnt());
|
|
// return
|
|
return ChData2.getPresentChunk(iRetrieveChunk);
|
|
}
|
|
|
|
bool C4Network2ResChunkData::MergeRanges(ChunkRange *pRange)
|
|
{
|
|
// no next entry?
|
|
if (!pRange || !pRange->Next) return false;
|
|
// do merge?
|
|
ChunkRange *pNext = pRange->Next;
|
|
if (pRange->Start + pRange->Length < pNext->Start) return false;
|
|
// get overlap
|
|
int32_t iOverlap = std::min((pRange->Start + pRange->Length) - pNext->Start, pNext->Length);
|
|
// set new chunk range
|
|
pRange->Length += pNext->Length - iOverlap;
|
|
// remove range
|
|
pRange->Next = pNext->Next;
|
|
delete pNext;
|
|
// counts
|
|
iChunkRangeCnt--; iPresentChunkCnt -= iOverlap;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
void C4Network2ResChunkData::GetNegative(C4Network2ResChunkData &Target) const
|
|
{
|
|
// clear target
|
|
Target.SetIncomplete(iChunkCnt);
|
|
// add all ranges that are missing
|
|
int32_t iFreeStart = 0;
|
|
for (ChunkRange *pRange = pChunkRanges; pRange; pRange = pRange->Next)
|
|
{
|
|
// add range
|
|
Target.AddChunkRange(iFreeStart, pRange->Start - iFreeStart);
|
|
// safe new start
|
|
iFreeStart = pRange->Start + pRange->Length;
|
|
}
|
|
// add last range
|
|
Target.AddChunkRange(iFreeStart, iChunkCnt - iFreeStart);
|
|
}
|
|
|
|
int32_t C4Network2ResChunkData::getPresentChunk(int32_t iNr) const
|
|
{
|
|
for (ChunkRange *pRange = pChunkRanges; pRange; pRange = pRange->Next)
|
|
if (iNr < pRange->Length)
|
|
return iNr + pRange->Start;
|
|
else
|
|
iNr -= pRange->Length;
|
|
return -1;
|
|
}
|
|
|
|
void C4Network2ResChunkData::CompileFunc(StdCompiler *pComp)
|
|
{
|
|
bool deserializing = pComp->isDeserializer();
|
|
if (deserializing) Clear();
|
|
// Data
|
|
pComp->Value(mkNamingAdapt(mkIntPackAdapt(iChunkCnt), "ChunkCnt", 0));
|
|
pComp->Value(mkNamingAdapt(mkIntPackAdapt(iChunkRangeCnt), "ChunkRangeCnt", 0));
|
|
// Ranges
|
|
if (!pComp->Name("Ranges"))
|
|
pComp->excCorrupt("ResChunk ranges expected!");
|
|
ChunkRange *pRange = nullptr;
|
|
for (int32_t i = 0; i < iChunkRangeCnt; i++)
|
|
{
|
|
// Create new range / go to next range
|
|
if (deserializing)
|
|
pRange = (pRange ? pRange->Next : pChunkRanges) = new ChunkRange;
|
|
else
|
|
pRange = pRange ? pRange->Next : pChunkRanges;
|
|
// Separate
|
|
if (i) pComp->Separator();
|
|
// Compile range
|
|
pComp->Value(mkIntPackAdapt(pRange->Start));
|
|
pComp->Separator(StdCompiler::SEP_PART2);
|
|
pComp->Value(mkIntPackAdapt(pRange->Length));
|
|
}
|
|
// Terminate list
|
|
if (deserializing)
|
|
(pRange ? pRange->Next : pChunkRanges) = nullptr;
|
|
pComp->NameEnd();
|
|
}
|
|
|
|
// *** C4Network2Res
|
|
|
|
C4Network2Res::C4Network2Res(C4Network2ResList *pnParent)
|
|
: fDirty(false),
|
|
fTempFile(false), fStandaloneFailed(false),
|
|
iRefCnt(0), fRemoved(false),
|
|
iLastReqTime(0),
|
|
fLoading(false),
|
|
pCChunks(nullptr), iDiscoverStartTime(0), pLoads(nullptr), iLoadCnt(0),
|
|
pNext(nullptr),
|
|
pParent(pnParent)
|
|
{
|
|
szFile[0] = szStandalone[0] = '\0';
|
|
}
|
|
|
|
C4Network2Res::~C4Network2Res()
|
|
{
|
|
assert(!pNext);
|
|
Clear();
|
|
}
|
|
|
|
bool C4Network2Res::SetByFile(const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fSilent)
|
|
{
|
|
CStdLock FileLock(&FileCSec);
|
|
// default resource name: relative path
|
|
if (!szResName) szResName = Config.AtRelativePath(strFilePath);
|
|
SCopy(strFilePath, szFile, sizeof(szFile)-1);
|
|
// group?
|
|
C4Group Grp;
|
|
if (Reloc.Open(Grp, strFilePath))
|
|
return SetByGroup(&Grp, fTemp, eType, iResID, szResName, fSilent);
|
|
// so it needs to be a file
|
|
StdStrBuf szFullFile;
|
|
if (!Reloc.LocateItem(szFile, szFullFile))
|
|
{ if (!fSilent) LogF("SetByFile: file %s not found!", strFilePath); return false; }
|
|
// calc checksum
|
|
uint32_t iCRC32;
|
|
if (!GetFileCRC(szFullFile.getData(), &iCRC32)) return false;
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
// log
|
|
LogSilentF("Network: Resource: complete %d:%s is file %s (%s)", iResID, szResName, szFile, fTemp ? "temp" : "static");
|
|
#endif
|
|
// set core
|
|
Core.Set(eType, iResID, Config.AtRelativePath(szFullFile.getData()), iCRC32);
|
|
// set own data
|
|
fDirty = true;
|
|
fTempFile = fTemp;
|
|
fStandaloneFailed = false;
|
|
fRemoved = false;
|
|
iLastReqTime = time(nullptr);
|
|
fLoading = false;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::SetByGroup(C4Group *pGrp, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fSilent) // by main thread
|
|
{
|
|
Clear();
|
|
CStdLock FileLock(&FileCSec);
|
|
// default resource name: relative path
|
|
StdStrBuf sResName;
|
|
if (szResName)
|
|
sResName = szResName;
|
|
else
|
|
{
|
|
StdStrBuf sFullName = pGrp->GetFullName();
|
|
sResName.Copy(Config.AtRelativePath(sFullName.getData()));
|
|
}
|
|
SCopy(pGrp->GetFullName().getData(), szFile, sizeof(szFile)-1);
|
|
// set core
|
|
Core.Set(eType, iResID, sResName.getData(), pGrp->EntryCRC32());
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
// log
|
|
LogSilentF("Network: Resource: complete %d:%s is file %s (%s)", iResID, sResName.getData(), szFile, fTemp ? "temp" : "static");
|
|
#endif
|
|
// set data
|
|
fDirty = true;
|
|
fTempFile = fTemp;
|
|
fStandaloneFailed = false;
|
|
fRemoved = false;
|
|
iLastReqTime = time(nullptr);
|
|
fLoading = false;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::SetByCore(const C4Network2ResCore &nCore, bool fSilent, const char *szAsFilename, int32_t iRecursion) // by main thread
|
|
{
|
|
StdStrBuf sFilename;
|
|
// try open local file
|
|
const char *szFilename = szAsFilename ? szAsFilename : GetC4Filename(nCore.getFileName());
|
|
if (SetByFile(szFilename, false, nCore.getType(), nCore.getID(), nCore.getFileName(), fSilent))
|
|
{
|
|
// check contents checksum
|
|
if (Core.getContentsCRC() == nCore.getContentsCRC())
|
|
{
|
|
// set core
|
|
fDirty = true;
|
|
Core = nCore;
|
|
// ok then
|
|
return true;
|
|
}
|
|
}
|
|
// get and search for filename without specified folder (e.g., Castle.ocs when the opened game is Easy.ocf\Castle.ocs)
|
|
const char *szFilenameOnly = GetFilename(szFilename);
|
|
const char *szFilenameC4 = GetC4Filename(szFilename);
|
|
if (szFilenameOnly != szFilenameC4)
|
|
{
|
|
sFilename.Copy(szFilename, SLen(szFilename) - SLen(szFilenameC4));
|
|
sFilename.Append(szFilenameOnly);
|
|
if (SetByCore(nCore, fSilent, szFilenameOnly, Config.Network.MaxResSearchRecursion)) return true;
|
|
}
|
|
// if it could still not be set, try within all folders of root (ignoring special folders), and try as file outside the folder
|
|
// but do not recurse any deeper than set by config (default: One folder)
|
|
if (iRecursion >= Config.Network.MaxResSearchRecursion) return false;
|
|
StdStrBuf sSearchPath; const char *szSearchPath;
|
|
if (!iRecursion)
|
|
szSearchPath = Config.General.ExePath.getData();
|
|
else
|
|
{
|
|
sSearchPath.Copy(szFilename, SLen(szFilename) - SLen(szFilenameC4));
|
|
szSearchPath = sSearchPath.getData();
|
|
}
|
|
StdStrBuf sNetPath; sNetPath.Copy(Config.Network.WorkPath);
|
|
char *szNetPath = sNetPath.GrabPointer();
|
|
TruncateBackslash(szNetPath);
|
|
sNetPath.Take(szNetPath);
|
|
for (DirectoryIterator i(szSearchPath); *i; ++i)
|
|
if (DirectoryExists(*i))
|
|
if (!*GetExtension(*i)) // directories without extension only
|
|
if (!szNetPath || !*szNetPath || !ItemIdentical(*i, szNetPath)) // ignore network path
|
|
{
|
|
// search for complete name at subpath (e.g. MyFolder\Easy.ocf\Castle.ocs)
|
|
sFilename.Format("%s%c%s", *i, DirectorySeparator, szFilenameC4);
|
|
if (SetByCore(nCore, fSilent, sFilename.getData(), iRecursion + 1))
|
|
return true;
|
|
}
|
|
// file could not be found locally
|
|
return false;
|
|
}
|
|
|
|
bool C4Network2Res::SetLoad(const C4Network2ResCore &nCore) // by main thread
|
|
{
|
|
Clear();
|
|
CStdLock FileLock(&FileCSec);
|
|
// must be loadable
|
|
if (!nCore.isLoadable()) return false;
|
|
// save core, set chunks
|
|
Core = nCore;
|
|
Chunks.SetIncomplete(Core.getChunkCnt());
|
|
// create temporary file
|
|
if (!pParent->FindTempResFileName(Core.getFileName(), szFile))
|
|
return false;
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
// log
|
|
Application.InteractiveThread.ThreadLogS("Network: Resource: loading %d:%s to file %s", Core.getID(), Core.getFileName(), szFile);
|
|
#endif
|
|
// set standalone (result is going to be binary-compatible)
|
|
SCopy(szFile, szStandalone, sizeof(szStandalone) - 1);
|
|
// set flags
|
|
fDirty = false;
|
|
fTempFile = true;
|
|
fStandaloneFailed = false;
|
|
fRemoved = false;
|
|
iLastReqTime = time(nullptr);
|
|
fLoading = true;
|
|
// No discovery yet
|
|
iDiscoverStartTime = 0;
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::SetDerived(const char *strName, const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iDResID)
|
|
{
|
|
Clear();
|
|
CStdLock FileLock(&FileCSec);
|
|
// set core
|
|
Core.Set(eType, C4NetResIDAnonymous, strName, ~0);
|
|
Core.SetDerived(iDResID);
|
|
// save file path
|
|
SCopy(strFilePath, szFile, _MAX_PATH);
|
|
*szStandalone = '\0';
|
|
// set flags
|
|
fDirty = false;
|
|
fTempFile = fTemp;
|
|
fStandaloneFailed = false;
|
|
fRemoved = false;
|
|
iLastReqTime = time(nullptr);
|
|
fLoading = false;
|
|
// Do not set any chunk data - anonymous resources are very likely to change.
|
|
// Wait for FinishDerived()-call.
|
|
return true;
|
|
}
|
|
|
|
void C4Network2Res::ChangeID(int32_t inID)
|
|
{
|
|
Core.SetID(inID);
|
|
}
|
|
|
|
bool C4Network2Res::IsBinaryCompatible()
|
|
{
|
|
// returns wether the standalone of this resource is binary compatible
|
|
// to the official version (means: matches the file checksum)
|
|
|
|
CStdLock FileLock(&FileCSec);
|
|
// standalone set? ok then (see GetStandalone)
|
|
if (szStandalone[0]) return true;
|
|
// is a directory?
|
|
if (DirectoryExists(szFile))
|
|
// forget it - if the file is packed now, the creation time and author
|
|
// won't match.
|
|
return false;
|
|
// try to create the standalone
|
|
return GetStandalone(nullptr, 0, false, false, true);
|
|
}
|
|
|
|
bool C4Network2Res::GetStandalone(char *pTo, int32_t iMaxL, bool fSetOfficial, bool fAllowUnloadable, bool fSilent)
|
|
{
|
|
// already set?
|
|
if (szStandalone[0])
|
|
{
|
|
if (pTo) SCopy(szStandalone, pTo, iMaxL);
|
|
return true;
|
|
}
|
|
// already tried and failed? No point in retrying
|
|
if (fStandaloneFailed) return false;
|
|
// not loadable? Wo won't be able to check the standalone as the core will lack the needed information.
|
|
// the standalone won't be interesting in this case, anyway.
|
|
if (!fSetOfficial && !Core.isLoadable()) return false;
|
|
// set flag, so failure below will let future calls fail
|
|
fStandaloneFailed = true;
|
|
// lock file
|
|
CStdLock FileLock(&FileCSec);
|
|
|
|
// directory?
|
|
SCopy(szFile, szStandalone, sizeof(szStandalone)-1);
|
|
if (DirectoryExists(szFile))
|
|
{
|
|
// size check for the directory, if allowed
|
|
if (fAllowUnloadable)
|
|
{
|
|
uint32_t iDirSize;
|
|
if (!DirSizeHelper::GetDirSize(szFile, &iDirSize, Config.Network.MaxLoadFileSize))
|
|
{ if (!fSilent) LogF("Network: could not get directory size of %s!", szFile); szStandalone[0] = '\0'; return false; }
|
|
if (iDirSize > uint32_t(Config.Network.MaxLoadFileSize))
|
|
{ if (!fSilent) LogSilentF("Network: %s over size limit, will be marked unloadable!", szFile); szStandalone[0] = '\0'; return false; }
|
|
}
|
|
// log - this may take a few seconds
|
|
if (!fSilent) LogF(LoadResStr("IDS_PRC_NETPACKING"), GetFilename(szFile));
|
|
// pack inplace?
|
|
if (!fTempFile)
|
|
{
|
|
if (!pParent->FindTempResFileName(szFile, szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: could not find free name for temporary file!"); szStandalone[0] = '\0'; return false; }
|
|
if (!C4Group_PackDirectoryTo(szFile, szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: could not pack directory!"); szStandalone[0] = '\0'; return false; }
|
|
}
|
|
else if (!C4Group_PackDirectory(szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: could not pack directory!"); if (!SEqual(szFile, szStandalone)) EraseDirectory(szStandalone); szStandalone[0] = '\0'; return false; }
|
|
// make sure directory is packed
|
|
if (DirectoryExists(szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: directory hasn't been packed!"); if (!SEqual(szFile, szStandalone)) EraseDirectory(szStandalone); szStandalone[0] = '\0'; return false; }
|
|
// fallthru
|
|
}
|
|
|
|
// doesn't exist physically?
|
|
if (!FileExists(szStandalone))
|
|
{
|
|
// try C4Group (might be packed)
|
|
if (!pParent->FindTempResFileName(szFile, szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: could not find free name for temporary file!"); szStandalone[0] = '\0'; return false; }
|
|
if (!C4Group_CopyItem(szFile, szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: could not copy to temporary file!"); szStandalone[0] = '\0'; return false; }
|
|
}
|
|
|
|
// remains missing? give up.
|
|
if (!FileExists(szStandalone))
|
|
{ if (!fSilent) Log("GetStandalone: file not found!"); szStandalone[0] = '\0'; return false; }
|
|
|
|
// do optimizations (delete unneeded entries)
|
|
if (!OptimizeStandalone(fSilent))
|
|
{ if (!SEqual(szFile, szStandalone)) EraseItem(szStandalone); szStandalone[0] = '\0'; return false; }
|
|
|
|
// get file size
|
|
size_t iSize = FileSize(szStandalone);
|
|
// size limit
|
|
if (fAllowUnloadable)
|
|
if (iSize > uint32_t(Config.Network.MaxLoadFileSize))
|
|
{ if (!fSilent) LogSilentF("Network: %s over size limit, will be marked unloadable!", szFile); szStandalone[0] = '\0'; return false; }
|
|
// check
|
|
if (!fSetOfficial && iSize != Core.getFileSize())
|
|
{
|
|
// remove file
|
|
if (!SEqual(szFile, szStandalone)) EraseItem(szStandalone);
|
|
szStandalone[0] = '\0';
|
|
// sorry, this version isn't good enough :(
|
|
return false;
|
|
}
|
|
|
|
// calc checksum
|
|
uint32_t iCRC32;
|
|
if (!GetFileCRC(szStandalone, &iCRC32))
|
|
{ if (!fSilent) Log("GetStandalone: could not calculate checksum!"); return false; }
|
|
// set / check
|
|
if (!fSetOfficial && iCRC32 != Core.getFileCRC())
|
|
{
|
|
// remove file, return
|
|
if (!SEqual(szFile, szStandalone)) EraseItem(szStandalone);
|
|
szStandalone[0] = '\0';
|
|
return false;
|
|
}
|
|
|
|
// we didn't fail
|
|
fStandaloneFailed = false;
|
|
// mark resource as loadable and safe file information
|
|
Core.SetLoadable(iSize, iCRC32);
|
|
// set up chunk data
|
|
Chunks.SetComplete(Core.getChunkCnt());
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::CalculateSHA()
|
|
{
|
|
// already present?
|
|
if (Core.hasFileSHA()) return true;
|
|
// get the file
|
|
char szStandalone[_MAX_PATH + 1];
|
|
if (!GetStandalone(szStandalone, _MAX_PATH, false))
|
|
SCopy(szFile, szStandalone, _MAX_PATH);
|
|
// get the hash
|
|
BYTE hash[SHA_DIGEST_LENGTH];
|
|
if (!GetFileSHA1(szStandalone, hash))
|
|
return false;
|
|
// save it back
|
|
Core.SetFileSHA(hash);
|
|
// okay
|
|
return true;
|
|
}
|
|
|
|
|
|
C4Network2Res::Ref C4Network2Res::Derive()
|
|
{
|
|
// Called before the file is changed. Rescues all files and creates a
|
|
// new resource for the file. This resource is flagged as "anonymous", as it
|
|
// has no official core (no res ID, to be exact).
|
|
// The resource gets its final core when FinishDerive() is called.
|
|
|
|
// For security: This doesn't make much sense if the resource is currently being
|
|
// loaded. So better assume the caller doesn't know what he's doing and check.
|
|
if (isLoading()) return nullptr;
|
|
|
|
CStdLock FileLock(&FileCSec);
|
|
// Save back original file name
|
|
char szOrgFile[_MAX_PATH+1];
|
|
SCopy(szFile, szOrgFile, _MAX_PATH);
|
|
bool fOrgTempFile = fTempFile;
|
|
|
|
// Create a copy of the file, if neccessary
|
|
if (!*szStandalone || SEqual(szStandalone, szFile))
|
|
{
|
|
if (!pParent->FindTempResFileName(szOrgFile, szFile))
|
|
{ Log("Derive: could not find free name for temporary file!"); return nullptr; }
|
|
if (!C4Group_CopyItem(szOrgFile, szFile))
|
|
{ Log("Derive: could not copy to temporary file!"); return nullptr; }
|
|
// set standalone
|
|
if (*szStandalone)
|
|
SCopy(szFile, szStandalone, _MAX_PATH);
|
|
fTempFile = true;
|
|
}
|
|
else
|
|
{
|
|
// Standlone exists: Just set szFile to point on the standlone. It's
|
|
// assumed that the original file isn't of intrest after this point anyway.
|
|
SCopy(szStandalone, szFile, _MAX_PATH);
|
|
fTempFile = true;
|
|
}
|
|
|
|
Application.InteractiveThread.ThreadLogS("Network: Resource: deriving from %d:%s, original at %s", getResID(), Core.getFileName(), szFile);
|
|
|
|
// (note: should remove temp file if something fails after this point)
|
|
|
|
// create new resource
|
|
C4Network2Res::Ref pDRes = new C4Network2Res(pParent);
|
|
if (!pDRes) return nullptr;
|
|
|
|
// initialize
|
|
if (!pDRes->SetDerived(Core.getFileName(), szOrgFile, fOrgTempFile, getType(), getResID()))
|
|
return nullptr;
|
|
|
|
// add to list
|
|
pParent->Add(pDRes);
|
|
|
|
// return new resource
|
|
return pDRes;
|
|
}
|
|
|
|
bool C4Network2Res::FinishDerive() // by main thread
|
|
{
|
|
// All changes have been made. Register this resource with a new ID.
|
|
|
|
// security
|
|
if (!isAnonymous()) return false;
|
|
|
|
CStdLock FileLock(&FileCSec);
|
|
// Save back data
|
|
int32_t iDerID = Core.getDerID();
|
|
char szName[_MAX_PATH+1]; SCopy(Core.getFileName(), szName, _MAX_PATH);
|
|
char szFileC[_MAX_PATH+1]; SCopy(szFile, szFileC, _MAX_PATH);
|
|
// Set by file
|
|
if (!SetByFile(szFileC, fTempFile, getType(), pParent->nextResID(), szName))
|
|
return false;
|
|
// create standalone
|
|
if (!GetStandalone(nullptr, 0, true))
|
|
return false;
|
|
// Set ID
|
|
Core.SetDerived(iDerID);
|
|
// announce derive
|
|
pParent->getIOClass()->BroadcastMsg(MkC4NetIOPacket(PID_NetResDerive, Core));
|
|
// derivation is dirty bussines
|
|
fDirty = true;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::FinishDerive(const C4Network2ResCore &nCore)
|
|
{
|
|
// security
|
|
if (!isAnonymous()) return false;
|
|
// Set core
|
|
Core = nCore;
|
|
// Set chunks (assume the resource is complete)
|
|
Chunks.SetComplete(Core.getChunkCnt());
|
|
|
|
// Note that the Contents-CRC is /not/ checked. Derivation needs to be
|
|
// synchronized outside of C4Network2Res.
|
|
|
|
// But note that the resource /might/ be binary compatible (though very
|
|
// unlikely), so do not set fNotBinaryCompatible.
|
|
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
C4Group *C4Network2Res::OpenAsGrp() const
|
|
{
|
|
C4Group *pnGrp = new C4Group();
|
|
if (!pnGrp->Open(szFile))
|
|
{
|
|
delete pnGrp;
|
|
return nullptr;
|
|
}
|
|
return pnGrp;
|
|
}
|
|
|
|
void C4Network2Res::Remove()
|
|
{
|
|
// schedule for removal
|
|
fRemoved = true;
|
|
}
|
|
|
|
bool C4Network2Res::SendStatus(C4Network2IOConnection *pTo)
|
|
{
|
|
// pack status
|
|
C4NetIOPacket Pkt = MkC4NetIOPacket(PID_NetResStat, C4PacketResStatus(Core.getID(), Chunks));
|
|
// to one client?
|
|
if (pTo)
|
|
return pTo->Send(Pkt);
|
|
else
|
|
{
|
|
// reset dirty flag
|
|
fDirty = false;
|
|
// broadcast status
|
|
assert(pParent && pParent->getIOClass());
|
|
return pParent->getIOClass()->BroadcastMsg(Pkt);
|
|
}
|
|
}
|
|
|
|
bool C4Network2Res::SendChunk(uint32_t iChunk, int32_t iToClient)
|
|
{
|
|
assert(pParent && pParent->getIOClass());
|
|
if (!szStandalone[0] || iChunk >= Core.getChunkCnt()) return false;
|
|
// find connection for given client (one of the rare uses of the data connection)
|
|
C4Network2IOConnection *pConn = pParent->getIOClass()->GetDataConnection(iToClient);
|
|
if (!pConn) return false;
|
|
// save last request time
|
|
iLastReqTime = time(nullptr);
|
|
// create packet
|
|
CStdLock FileLock(&FileCSec);
|
|
C4Network2ResChunk ResChunk;
|
|
ResChunk.Set(this, iChunk);
|
|
// send
|
|
bool fSuccess = pConn->Send(MkC4NetIOPacket(PID_NetResData, ResChunk));
|
|
pConn->DelRef();
|
|
return fSuccess;
|
|
}
|
|
|
|
void C4Network2Res::AddRef()
|
|
{
|
|
++iRefCnt;
|
|
}
|
|
|
|
void C4Network2Res::DelRef()
|
|
{
|
|
if (--iRefCnt == 0)
|
|
delete this;
|
|
}
|
|
|
|
void C4Network2Res::OnDiscover(C4Network2IOConnection *pBy)
|
|
{
|
|
if (!IsBinaryCompatible()) return;
|
|
// discovered
|
|
iLastReqTime = time(nullptr);
|
|
// send status back
|
|
SendStatus(pBy);
|
|
}
|
|
|
|
void C4Network2Res::OnStatus(const C4Network2ResChunkData &rChunkData, C4Network2IOConnection *pBy)
|
|
{
|
|
if (!fLoading) return;
|
|
// discovered a source: reset timeout
|
|
iDiscoverStartTime = 0;
|
|
// check if the chunk data is valid
|
|
if (rChunkData.getChunkCnt() != Chunks.getChunkCnt())
|
|
return;
|
|
// add chunk data
|
|
ClientChunks *pChunks;
|
|
for (pChunks = pCChunks; pChunks; pChunks = pChunks->Next)
|
|
if (pChunks->ClientID == pBy->getClientID())
|
|
break;
|
|
// not found? add
|
|
if (!pChunks)
|
|
{
|
|
pChunks = new ClientChunks();
|
|
pChunks->Next = pCChunks;
|
|
pCChunks = pChunks;
|
|
}
|
|
pChunks->ClientID = pBy->getClientID();
|
|
pChunks->Chunks = rChunkData;
|
|
// check load
|
|
if (!StartLoad(pChunks->ClientID, pChunks->Chunks))
|
|
RemoveCChunks(pCChunks);
|
|
}
|
|
|
|
void C4Network2Res::OnChunk(const C4Network2ResChunk &rChunk)
|
|
{
|
|
if (!fLoading) return;
|
|
// correct resource?
|
|
if (rChunk.getResID() != getResID()) return;
|
|
// add resource data
|
|
CStdLock FileLock(&FileCSec);
|
|
bool fSuccess = rChunk.AddTo(this, pParent->getIOClass());
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
// log
|
|
Application.InteractiveThread.ThreadLogS("Network: Res: %s chunk %d to resource %s (%s)%s", fSuccess ? "added" : "could not add", rChunk.getChunkNr(), Core.getFileName(), szFile, fSuccess ? "" : "!");
|
|
#endif
|
|
if (fSuccess)
|
|
{
|
|
// status changed
|
|
fDirty = true;
|
|
// remove load waits
|
|
for (C4Network2ResLoad *pLoad = pLoads, *pNext; pLoad; pLoad = pNext)
|
|
{
|
|
pNext = pLoad->Next();
|
|
if (static_cast<uint32_t>(pLoad->getChunk()) == rChunk.getChunkNr())
|
|
RemoveLoad(pLoad);
|
|
}
|
|
}
|
|
// complete?
|
|
if (Chunks.isComplete())
|
|
EndLoad();
|
|
// check: start new loads?
|
|
else
|
|
StartNewLoads();
|
|
}
|
|
|
|
bool C4Network2Res::DoLoad()
|
|
{
|
|
if (!fLoading) return true;
|
|
// any loads currently active?
|
|
if (iLoadCnt)
|
|
{
|
|
// check for load timeouts
|
|
int32_t iLoadsRemoved = 0;
|
|
for (C4Network2ResLoad *pLoad = pLoads, *pNext; pLoad; pLoad = pNext)
|
|
{
|
|
pNext = pLoad->Next();
|
|
if (pLoad->CheckTimeout())
|
|
{
|
|
RemoveLoad(pLoad);
|
|
iLoadsRemoved++;
|
|
}
|
|
}
|
|
// start new loads
|
|
if (iLoadsRemoved) StartNewLoads();
|
|
}
|
|
else
|
|
{
|
|
// discover timeout?
|
|
if (iDiscoverStartTime)
|
|
if (difftime(time(nullptr), iDiscoverStartTime) > C4NetResDiscoverTimeout)
|
|
return false;
|
|
}
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2Res::NeedsDiscover()
|
|
{
|
|
// loading, but no active load sources?
|
|
if (fLoading && !iLoadCnt)
|
|
{
|
|
// set timeout, if this is the first discover
|
|
if (!iDiscoverStartTime)
|
|
iDiscoverStartTime = time(nullptr);
|
|
// do discover
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void C4Network2Res::Clear()
|
|
{
|
|
CStdLock FileLock(&FileCSec);
|
|
// delete files
|
|
if (fTempFile)
|
|
if (FileExists(szFile))
|
|
if (!EraseFile(szFile))
|
|
LogSilentF("Network: Could not delete temporary resource file (%s)", strerror(errno));
|
|
if (szStandalone[0] && !SEqual(szFile, szStandalone))
|
|
if (FileExists(szStandalone))
|
|
if (!EraseFile(szStandalone))
|
|
LogSilentF("Network: Could not delete temporary resource file (%s)", strerror(errno));
|
|
szFile[0] = szStandalone[0] = '\0';
|
|
fDirty = false;
|
|
fTempFile = false;
|
|
Core.Clear();
|
|
Chunks.Clear();
|
|
fRemoved = false;
|
|
ClearLoad();
|
|
}
|
|
|
|
int32_t C4Network2Res::OpenFileRead()
|
|
{
|
|
CStdLock FileLock(&FileCSec);
|
|
if (!GetStandalone(nullptr, 0, false, false, true)) return -1;
|
|
// FIXME: Use standard OC file access api here
|
|
#ifdef _WIN32
|
|
return _wopen(GetWideChar(szStandalone), _O_BINARY | O_RDONLY);
|
|
#else
|
|
return open(szStandalone, _O_BINARY | O_CLOEXEC | O_RDONLY);
|
|
#endif
|
|
}
|
|
|
|
int32_t C4Network2Res::OpenFileWrite()
|
|
{
|
|
CStdLock FileLock(&FileCSec);
|
|
// FIXME: Use standard OC file access api here
|
|
#ifdef _WIN32
|
|
return _wopen(GetWideChar(szStandalone), _O_BINARY | O_CREAT | O_WRONLY, S_IREAD | S_IWRITE);
|
|
#else
|
|
return open(szStandalone, _O_BINARY | O_CLOEXEC | O_CREAT | O_WRONLY, S_IREAD | S_IWRITE);
|
|
#endif
|
|
}
|
|
|
|
void C4Network2Res::StartNewLoads()
|
|
{
|
|
if (!pCChunks) return;
|
|
// count clients
|
|
int32_t iCChunkCnt = 0; ClientChunks *pChunks;
|
|
for (pChunks = pCChunks; pChunks; pChunks = pChunks->Next)
|
|
iCChunkCnt++;
|
|
// create array
|
|
ClientChunks **pC = new ClientChunks *[iCChunkCnt];
|
|
// initialize
|
|
int32_t i;
|
|
for (i = 0; i < iCChunkCnt; i++) pC[i] = nullptr;
|
|
// create shuffled order
|
|
for (pChunks = pCChunks, i = 0; i < iCChunkCnt; i++, pChunks = pChunks->Next)
|
|
{
|
|
// determine position
|
|
int32_t iPos = UnsyncedRandom(iCChunkCnt - i);
|
|
// find & set
|
|
for (int32_t j = 0; ; j++)
|
|
if (!pC[j] && !iPos--)
|
|
{
|
|
pC[j] = pChunks;
|
|
break;
|
|
}
|
|
}
|
|
// start new load until maximum count reached
|
|
while (iLoadCnt + 1 <= C4NetResMaxLoad)
|
|
{
|
|
int32_t ioLoadCnt = iLoadCnt;
|
|
// search someone
|
|
for (i = 0; i < iCChunkCnt; i++)
|
|
if (pC[i])
|
|
{
|
|
// try to start load
|
|
if (!StartLoad(pC[i]->ClientID, pC[i]->Chunks))
|
|
{ RemoveCChunks(pC[i]); pC[i] = nullptr; continue; }
|
|
// success?
|
|
if (iLoadCnt > ioLoadCnt) break;
|
|
}
|
|
// not found?
|
|
if (i >= iCChunkCnt)
|
|
break;
|
|
}
|
|
// clear up
|
|
delete [] pC;
|
|
}
|
|
|
|
bool C4Network2Res::StartLoad(int32_t iFromClient, const C4Network2ResChunkData &Available)
|
|
{
|
|
assert(pParent && pParent->getIOClass());
|
|
// all slots used? ignore
|
|
if (iLoadCnt + 1 >= C4NetResMaxLoad) return true;
|
|
// is there already a load by this client? ignore
|
|
for (C4Network2ResLoad *pPos = pLoads; pPos; pPos = pPos->Next())
|
|
if (pPos->getByClient() == iFromClient)
|
|
return true;
|
|
// find chunk to retrieve
|
|
int32_t iLoads[C4NetResMaxLoad]; int32_t i = 0;
|
|
for (C4Network2ResLoad *pLoad = pLoads; pLoad; pLoad = pLoad->Next())
|
|
iLoads[i++] = pLoad->getChunk();
|
|
int32_t iRetrieveChunk = Chunks.GetChunkToRetrieve(Available, i, iLoads);
|
|
// nothing? ignore
|
|
if (iRetrieveChunk < 0 || (uint32_t)iRetrieveChunk >= Core.getChunkCnt())
|
|
return true;
|
|
// search message connection for client
|
|
C4Network2IOConnection *pConn = pParent->getIOClass()->GetMsgConnection(iFromClient);
|
|
if (!pConn) return false;
|
|
// send request
|
|
if (!pConn->Send(MkC4NetIOPacket(PID_NetResReq, C4PacketResRequest(Core.getID(), iRetrieveChunk))))
|
|
{ pConn->DelRef(); return false; }
|
|
pConn->DelRef();
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
// log
|
|
Application.InteractiveThread.ThreadLogS("Network: Res: requesting chunk %d of %d:%s (%s) from client %d",
|
|
iRetrieveChunk, Core.getID(), Core.getFileName(), szFile, iFromClient);
|
|
#endif
|
|
// create load class
|
|
C4Network2ResLoad *pnLoad = new C4Network2ResLoad(iRetrieveChunk, iFromClient);
|
|
// add to list
|
|
pnLoad->pNext = pLoads;
|
|
pLoads = pnLoad;
|
|
iLoadCnt++;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
void C4Network2Res::EndLoad()
|
|
{
|
|
// clear loading data
|
|
ClearLoad();
|
|
// set complete
|
|
fLoading = false;
|
|
// call handler
|
|
assert(pParent);
|
|
pParent->OnResComplete(this);
|
|
}
|
|
|
|
void C4Network2Res::ClearLoad()
|
|
{
|
|
// remove client chunks and loads
|
|
fLoading = false;
|
|
while (pCChunks) RemoveCChunks(pCChunks);
|
|
while (pLoads) RemoveLoad(pLoads);
|
|
iDiscoverStartTime = iLoadCnt = 0;
|
|
}
|
|
|
|
void C4Network2Res::RemoveLoad(C4Network2ResLoad *pLoad)
|
|
{
|
|
if (pLoad == pLoads)
|
|
pLoads = pLoad->Next();
|
|
else
|
|
{
|
|
// find previous entry
|
|
C4Network2ResLoad *pPrev;
|
|
for (pPrev = pLoads; pPrev && pPrev->Next() != pLoad; pPrev = pPrev->Next()) {}
|
|
// remove
|
|
if (pPrev)
|
|
pPrev->pNext = pLoad->Next();
|
|
}
|
|
// delete
|
|
delete pLoad;
|
|
iLoadCnt--;
|
|
}
|
|
|
|
void C4Network2Res::RemoveCChunks(ClientChunks *pChunks)
|
|
{
|
|
if (pChunks == pCChunks)
|
|
pCChunks = pChunks->Next;
|
|
else
|
|
{
|
|
// find previous entry
|
|
ClientChunks *pPrev;
|
|
for (pPrev = pCChunks; pPrev && pPrev->Next != pChunks; pPrev = pPrev->Next) {}
|
|
// remove
|
|
if (pPrev)
|
|
pPrev->Next = pChunks->Next;
|
|
}
|
|
// delete
|
|
delete pChunks;
|
|
}
|
|
|
|
bool C4Network2Res::OptimizeStandalone(bool fSilent)
|
|
{
|
|
CStdLock FileLock(&FileCSec);
|
|
// for now: player files only
|
|
if (Core.getType() == NRT_Player)
|
|
{
|
|
// log - this may take a few seconds
|
|
if (!fSilent) LogF(LoadResStr("IDS_PRC_NETPREPARING"), GetFilename(szFile));
|
|
// copy to temp file, if needed
|
|
if (!fTempFile && SEqual(szFile, szStandalone))
|
|
{
|
|
char szNewStandalone[_MAX_PATH + 1];
|
|
if (!pParent->FindTempResFileName(szStandalone, szNewStandalone))
|
|
{ if (!fSilent) Log("OptimizeStandalone: could not find free name for temporary file!"); return false; }
|
|
if (!C4Group_CopyItem(szStandalone, szNewStandalone))
|
|
{ if (!fSilent) Log("OptimizeStandalone: could not copy to temporary file!"); return false; } /* TODO: Test failure */
|
|
SCopy(szNewStandalone, szStandalone, sizeof(szStandalone) - 1);
|
|
}
|
|
// open as group
|
|
C4Group Grp;
|
|
if (!Grp.Open(szStandalone))
|
|
{ if (!fSilent) Log("OptimizeStandalone: could not open player file!"); return false; }
|
|
// remove bigicon, if the file size is too large
|
|
size_t iBigIconSize=0;
|
|
if (Grp.FindEntry(C4CFN_BigIcon, nullptr, &iBigIconSize))
|
|
if (iBigIconSize > C4NetResMaxBigicon*1024)
|
|
Grp.Delete(C4CFN_BigIcon);
|
|
Grp.Close();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// *** C4Network2ResChunk
|
|
|
|
C4Network2ResChunk::C4Network2ResChunk() = default;
|
|
|
|
C4Network2ResChunk::~C4Network2ResChunk() = default;
|
|
|
|
bool C4Network2ResChunk::Set(C4Network2Res *pRes, uint32_t inChunk)
|
|
{
|
|
const C4Network2ResCore &Core = pRes->getCore();
|
|
iResID = pRes->getResID();
|
|
iChunk = inChunk;
|
|
// calculate offset and size
|
|
int32_t iOffset = iChunk * Core.getChunkSize(),
|
|
iSize = std::min<int32_t>(Core.getFileSize() - iOffset, C4NetResChunkSize);
|
|
if (iSize < 0) { LogF("Network: could not get chunk from offset %d from resource file %s: File size is only %d!", iOffset, pRes->getFile(), Core.getFileSize()); return false; }
|
|
// open file
|
|
int32_t f = pRes->OpenFileRead();
|
|
if (f == -1) { LogF("Network: could not open resource file %s!", pRes->getFile()); return false; }
|
|
// seek
|
|
if (iOffset)
|
|
if (lseek(f, iOffset, SEEK_SET) != iOffset)
|
|
{ close(f); LogF("Network: could not read resource file %s!", pRes->getFile()); return false; }
|
|
// read chunk of data
|
|
char *pBuf = (char *) malloc(iSize);
|
|
if (read(f, pBuf, iSize) != iSize)
|
|
{ free(pBuf); close(f); LogF("Network: could not read resource file %s!", pRes->getFile()); return false; }
|
|
// set
|
|
Data.Take(pBuf, iSize);
|
|
// close
|
|
close(f);
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2ResChunk::AddTo(C4Network2Res *pRes, C4Network2IO *pIO) const
|
|
{
|
|
assert(pRes); assert(pIO);
|
|
const C4Network2ResCore &Core = pRes->getCore();
|
|
// check
|
|
if (iResID != pRes->getResID())
|
|
{
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
Application.InteractiveThread.ThreadLogS("C4Network2ResChunk(%d)::AddTo(%s [%d]): Resource ID mismatch!", (int) iResID, (const char *) Core.getFileName(), (int) pRes->getResID());
|
|
#endif
|
|
return false;
|
|
}
|
|
// calculate offset and size
|
|
int32_t iOffset = iChunk * Core.getChunkSize();
|
|
if (iOffset + Data.getSize() > Core.getFileSize())
|
|
{
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
Application.InteractiveThread.ThreadLogS("C4Network2ResChunk(%d)::AddTo(%s [%d]): Adding %d bytes at offset %d exceeds expected file size of %d!", (int) iResID, (const char *) Core.getFileName(), (int) pRes->getResID(), (int) Data.getSize(), (int) iOffset, (int) Core.getFileSize());
|
|
#endif
|
|
return false;
|
|
}
|
|
// open file
|
|
int32_t f = pRes->OpenFileWrite();
|
|
if (f == -1)
|
|
{
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
Application.InteractiveThread.ThreadLogS("C4Network2ResChunk(%d)::AddTo(%s [%d]): Open write file error: %s!", (int) iResID, (const char *) Core.getFileName(), (int) pRes->getResID(), strerror(errno));
|
|
#endif
|
|
return false;
|
|
}
|
|
// seek
|
|
if (iOffset)
|
|
if (lseek(f, iOffset, SEEK_SET) != iOffset)
|
|
{
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
Application.InteractiveThread.ThreadLogS("C4Network2ResChunk(%d)::AddTo(%s [%d]): lseek file error: %s!", (int) iResID, (const char *) Core.getFileName(), (int) pRes->getResID(), strerror(errno));
|
|
#endif
|
|
close(f);
|
|
return false;
|
|
}
|
|
// write
|
|
if (write(f, Data.getData(), Data.getSize()) != int32_t(Data.getSize()))
|
|
{
|
|
#ifdef C4NET2RES_DEBUG_LOG
|
|
Application.InteractiveThread.ThreadLogS("C4Network2ResChunk(%d)::AddTo(%s [%d]): write error: %s!", (int) iResID, (const char *) Core.getFileName(), (int) pRes->getResID(), strerror(errno));
|
|
#endif
|
|
close(f);
|
|
return false;
|
|
}
|
|
// ok, add chunks
|
|
close(f);
|
|
pRes->Chunks.AddChunk(iChunk);
|
|
return true;
|
|
}
|
|
|
|
void C4Network2ResChunk::CompileFunc(StdCompiler *pComp)
|
|
{
|
|
// pack header
|
|
pComp->Value(mkNamingAdapt(iResID, "ResID", -1));
|
|
pComp->Value(mkNamingAdapt(iChunk, "Chunk", ~0U));
|
|
// Data
|
|
pComp->Value(mkNamingAdapt(Data, "Data"));
|
|
}
|
|
|
|
// *** C4Network2ResList
|
|
|
|
C4Network2ResList::C4Network2ResList()
|
|
: ResListCSec(this),
|
|
iNextResID((-1) << 16)
|
|
{
|
|
|
|
}
|
|
|
|
C4Network2ResList::~C4Network2ResList()
|
|
{
|
|
Clear();
|
|
}
|
|
|
|
bool C4Network2ResList::Init(int32_t inClientID, C4Network2IO *pIOClass) // by main thread
|
|
{
|
|
// clear old list
|
|
Clear();
|
|
// safe IO class
|
|
pIO = pIOClass;
|
|
// set client id
|
|
iNextResID = iClientID = 0;
|
|
SetLocalID(inClientID);
|
|
// create network path
|
|
if (!CreateNetworkFolder()) return false;
|
|
// ok
|
|
return true;
|
|
}
|
|
|
|
void C4Network2ResList::SetLocalID(int32_t inClientID)
|
|
{
|
|
CStdLock ResIDLock(&ResIDCSec);
|
|
int32_t iOldClientID = iClientID;
|
|
int32_t iIDDiff = (inClientID - iClientID) << 16;
|
|
// set new counter
|
|
iClientID = inClientID;
|
|
iNextResID += iIDDiff;
|
|
// change resource ids
|
|
CStdLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->getResClient() == iOldClientID)
|
|
pRes->ChangeID(pRes->getResID() + iIDDiff);
|
|
}
|
|
|
|
int32_t C4Network2ResList::nextResID() // by main thread
|
|
{
|
|
CStdLock ResIDLock(&ResIDCSec);
|
|
assert(iNextResID >= (iClientID << 16));
|
|
if (iNextResID >= ((iClientID+1) << 16) - 1)
|
|
iNextResID = std::max<int32_t>(0, iClientID) << 16;
|
|
// find free
|
|
while (getRes(iNextResID))
|
|
iNextResID++;
|
|
return iNextResID++;
|
|
}
|
|
|
|
C4Network2Res *C4Network2ResList::getRes(int32_t iResID)
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
|
|
if (pCur->getResID() == iResID)
|
|
return pCur;
|
|
return nullptr;
|
|
}
|
|
|
|
C4Network2Res *C4Network2ResList::getRes(const char *szFile, bool fLocalOnly)
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
|
|
if (!pCur->isAnonymous())
|
|
if (SEqual(pCur->getFile(), szFile))
|
|
if (!fLocalOnly || pCur->getResClient()==iClientID)
|
|
return pCur;
|
|
return nullptr;
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::getRefRes(int32_t iResID)
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
return getRes(iResID);
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::getRefRes(const char *szFile, bool fLocalOnly)
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
return getRes(szFile, fLocalOnly);
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::getRefNextRes(int32_t iResID)
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
C4Network2Res *pRes = nullptr;
|
|
for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
|
|
if (!pCur->isRemoved() && pCur->getResID() >= iResID)
|
|
if (!pRes || pRes->getResID() > pCur->getResID())
|
|
pRes = pCur;
|
|
return pRes;
|
|
}
|
|
|
|
void C4Network2ResList::Add(C4Network2Res *pRes)
|
|
{
|
|
// get locks
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
CStdLock ResListAddLock(&ResListAddCSec);
|
|
// reference
|
|
pRes->AddRef();
|
|
// add
|
|
pRes->pNext = pFirst;
|
|
pFirst = pRes;
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::AddByFile(const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fAllowUnloadable)
|
|
{
|
|
// already in list?
|
|
C4Network2Res::Ref pRes = getRefRes(strFilePath);
|
|
if (pRes) return pRes;
|
|
// get resource ID
|
|
if (iResID < 0) iResID = nextResID();
|
|
if (iResID < 0) { Log("AddByFile: no more resource IDs available!"); return nullptr; }
|
|
// create new
|
|
pRes = new C4Network2Res(this);
|
|
// initialize
|
|
if (!pRes->SetByFile(strFilePath, fTemp, eType, iResID, szResName)) { return nullptr; }
|
|
// create standalone for non-system files
|
|
// system files shouldn't create a standalone; they should never be marked loadable!
|
|
if (eType != NRT_System)
|
|
if (!pRes->GetStandalone(nullptr, 0, true, fAllowUnloadable))
|
|
if (!fAllowUnloadable)
|
|
{
|
|
delete pRes;
|
|
return nullptr;
|
|
}
|
|
// add to list
|
|
Add(pRes);
|
|
return pRes;
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::AddByGroup(C4Group *pGrp, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fAllowUnloadable)
|
|
{
|
|
// get resource ID
|
|
if (iResID < 0) iResID = nextResID();
|
|
if (iResID < 0) { Log("AddByGroup: no more resource IDs available!"); return nullptr; }
|
|
// create new
|
|
C4Network2Res::Ref pRes = new C4Network2Res(this);
|
|
// initialize
|
|
if (!pRes->SetByGroup(pGrp, fTemp, eType, iResID, szResName))
|
|
{
|
|
delete pRes;
|
|
return nullptr;
|
|
}
|
|
// create standalone
|
|
if (!pRes->GetStandalone(nullptr, 0, true, fAllowUnloadable))
|
|
if (!fAllowUnloadable)
|
|
{
|
|
delete pRes;
|
|
return nullptr;
|
|
}
|
|
// add to list
|
|
Add(pRes);
|
|
return pRes;
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::AddByCore(const C4Network2ResCore &Core, bool fLoad) // by main thread
|
|
{
|
|
// already in list?
|
|
C4Network2Res::Ref pRes = getRefRes(Core.getID());
|
|
if (pRes) return pRes;
|
|
#ifdef C4NET2RES_LOAD_ALL
|
|
// load without check (if possible)
|
|
if (Core.isLoadable()) return AddLoad(Core);
|
|
#endif
|
|
// create new
|
|
pRes = new C4Network2Res(this);
|
|
// try set by core
|
|
if (!pRes->SetByCore(Core, true))
|
|
{
|
|
pRes.Clear();
|
|
// try load (if specified)
|
|
return fLoad ? AddLoad(Core) : nullptr;
|
|
}
|
|
// log
|
|
Application.InteractiveThread.ThreadLogS("Network: Found identical %s. Not loading.", pRes->getCore().getFileName());
|
|
// add to list
|
|
Add(pRes);
|
|
// ok
|
|
return pRes;
|
|
}
|
|
|
|
C4Network2Res::Ref C4Network2ResList::AddLoad(const C4Network2ResCore &Core) // by main thread
|
|
{
|
|
// marked unloadable by creator?
|
|
if (!Core.isLoadable())
|
|
{
|
|
// show error msg
|
|
Application.InteractiveThread.ThreadLog("Network: Cannot load %s (marked unloadable)", Core.getFileName());
|
|
return nullptr;
|
|
}
|
|
// create new
|
|
C4Network2Res::Ref pRes = new C4Network2Res(this);
|
|
// initialize
|
|
pRes->SetLoad(Core);
|
|
// log
|
|
Application.InteractiveThread.ThreadLogS("Network: loading %s...", Core.getFileName());
|
|
// add to list
|
|
Add(pRes);
|
|
return pRes;
|
|
}
|
|
|
|
void C4Network2ResList::RemoveAtClient(int32_t iClientID) // by main thread
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->getResClient() == iClientID)
|
|
pRes->Remove();
|
|
}
|
|
|
|
void C4Network2ResList::Clear()
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
{
|
|
pRes->Remove();
|
|
pRes->iLastReqTime = 0;
|
|
}
|
|
iClientID = C4ClientIDUnknown;
|
|
iLastDiscover = iLastStatus = 0;
|
|
}
|
|
|
|
void C4Network2ResList::OnClientConnect(C4Network2IOConnection *pConn) // by main thread
|
|
{
|
|
// discover resources
|
|
SendDiscover(pConn);
|
|
}
|
|
|
|
void C4Network2ResList::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2IOConnection *pConn)
|
|
{
|
|
// security
|
|
if (!pConn) return;
|
|
|
|
#define GETPKT(type, name) \
|
|
assert(pPacket); const type &name = \
|
|
static_cast<const type &>(*pPacket);
|
|
|
|
switch (cStatus)
|
|
{
|
|
|
|
case PID_NetResDis: // resource discover
|
|
{
|
|
if (!pConn->isOpen()) break;
|
|
GETPKT(C4PacketResDiscover, Pkt);
|
|
// search matching resources
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (Pkt.isIDPresent(pRes->getResID()))
|
|
// must be binary compatible
|
|
if (pRes->IsBinaryCompatible())
|
|
pRes->OnDiscover(pConn);
|
|
}
|
|
break;
|
|
|
|
case PID_NetResStat: // resource status
|
|
{
|
|
if (!pConn->isOpen()) break;
|
|
GETPKT(C4PacketResStatus, Pkt);
|
|
// matching resource?
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
C4Network2Res *pRes = getRes(Pkt.getResID());
|
|
// present / being loaded? call handler
|
|
if (pRes)
|
|
pRes->OnStatus(Pkt.getChunks(), pConn);
|
|
}
|
|
break;
|
|
|
|
case PID_NetResDerive: // resource derive
|
|
{
|
|
GETPKT(C4Network2ResCore, Core);
|
|
if (Core.getDerID() < 0) break;
|
|
// Check if there is a anonymous derived resource with matching parent.
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->isAnonymous() && pRes->getCore().getDerID() == Core.getDerID())
|
|
pRes->FinishDerive(Core);
|
|
}
|
|
break;
|
|
|
|
case PID_NetResReq: // resource request
|
|
{
|
|
GETPKT(C4PacketResRequest, Pkt);
|
|
// find resource
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
C4Network2Res *pRes = getRes(Pkt.getReqID());
|
|
// send requested chunk
|
|
if (pRes && pRes->IsBinaryCompatible()) pRes->SendChunk(Pkt.getReqChunk(), pConn->getClientID());
|
|
}
|
|
break;
|
|
|
|
case PID_NetResData: // a chunk of data is coming in
|
|
{
|
|
GETPKT(C4Network2ResChunk, Chunk);
|
|
// find resource
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
C4Network2Res *pRes = getRes(Chunk.getResID());
|
|
// send requested chunk
|
|
if (pRes) pRes->OnChunk(Chunk);
|
|
}
|
|
break;
|
|
}
|
|
#undef GETPKT
|
|
}
|
|
|
|
void C4Network2ResList::OnTimer()
|
|
{
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
C4Network2Res *pRes;
|
|
// do loads, check timeouts
|
|
for (pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->isLoading() && !pRes->isRemoved())
|
|
if (!pRes->DoLoad())
|
|
pRes->Remove();
|
|
// discovery time?
|
|
if (!iLastDiscover || difftime(time(nullptr), iLastDiscover) >= C4NetResDiscoverInterval)
|
|
{
|
|
// needed?
|
|
bool fSendDiscover = false;
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->isLoading() && !pRes->isRemoved())
|
|
fSendDiscover |= pRes->NeedsDiscover();
|
|
// send
|
|
if (fSendDiscover)
|
|
SendDiscover();
|
|
}
|
|
// status update?
|
|
if (!iLastStatus || difftime(time(nullptr), iLastStatus) >= C4NetResStatusInterval)
|
|
{
|
|
// any?
|
|
bool fStatusUpdates = false;
|
|
for (pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (pRes->isDirty() && !pRes->isRemoved())
|
|
fStatusUpdates |= pRes->SendStatus();
|
|
// set time accordingly
|
|
iLastStatus = fStatusUpdates ? time(nullptr) : 0;
|
|
}
|
|
}
|
|
|
|
void C4Network2ResList::OnShareFree(CStdCSecEx *pCSec)
|
|
{
|
|
if (pCSec == &ResListCSec)
|
|
{
|
|
// remove entries
|
|
for (C4Network2Res *pRes = pFirst, *pNext, *pPrev = nullptr; pRes; pRes = pNext)
|
|
{
|
|
pNext = pRes->pNext;
|
|
if (pRes->isRemoved() && (!pRes->getLastReqTime() || difftime(time(nullptr), pRes->getLastReqTime()) > C4NetResDeleteTime))
|
|
{
|
|
// unlink
|
|
(pPrev ? pPrev->pNext : pFirst) = pNext;
|
|
// remove
|
|
pRes->pNext = nullptr;
|
|
pRes->DelRef();
|
|
}
|
|
else
|
|
pPrev = pRes;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool C4Network2ResList::SendDiscover(C4Network2IOConnection *pTo) // by both
|
|
{
|
|
// make packet
|
|
C4PacketResDiscover Pkt;
|
|
// add special retrieves
|
|
CStdShareLock ResListLock(&ResListCSec);
|
|
for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
|
|
if (!pRes->isRemoved())
|
|
if (pRes->isLoading())
|
|
Pkt.AddDisID(pRes->getResID());
|
|
ResListLock.Clear();
|
|
// empty?
|
|
if (!Pkt.getDisIDCnt()) return false;
|
|
// broadcast?
|
|
if (!pTo)
|
|
{
|
|
// save time
|
|
iLastDiscover = time(nullptr);
|
|
// send
|
|
return pIO->BroadcastMsg(MkC4NetIOPacket(PID_NetResDis, Pkt));
|
|
}
|
|
else
|
|
return pTo->Send(MkC4NetIOPacket(PID_NetResDis, Pkt));
|
|
}
|
|
|
|
void C4Network2ResList::OnResComplete(C4Network2Res *pRes)
|
|
{
|
|
// log (network thread -> ThreadLog)
|
|
Application.InteractiveThread.ThreadLogS("Network: %s received.", pRes->getCore().getFileName());
|
|
// call handler (ctrl might wait for this resource)
|
|
::Control.Network.OnResComplete(pRes);
|
|
}
|
|
|
|
bool C4Network2ResList::CreateNetworkFolder()
|
|
{
|
|
// get network path without trailing backslash
|
|
char szNetworkPath[_MAX_PATH+1];
|
|
SCopy(Config.AtNetworkPath(""), szNetworkPath, _MAX_PATH);
|
|
TruncateBackslash(szNetworkPath);
|
|
// but make sure that the configured path has one
|
|
AppendBackslash(Config.Network.WorkPath);
|
|
// does not exist?
|
|
if (!DirectoryExists(szNetworkPath))
|
|
{
|
|
if (!CreatePath(szNetworkPath))
|
|
{ LogFatal("Network: could not create network path!"); return false; }
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool C4Network2ResList::FindTempResFileName(const char *szFilename, char *pTarget)
|
|
{
|
|
char safeFilename[_MAX_PATH];
|
|
char* safePos = safeFilename;
|
|
while (*szFilename)
|
|
{
|
|
if ((*szFilename >= 'a' && *szFilename <= 'z') ||
|
|
(*szFilename >= 'A' && *szFilename <= 'Z') ||
|
|
(*szFilename >= '0' && *szFilename <= '9') ||
|
|
(*szFilename == '.') || (*szFilename == '/'))
|
|
*safePos = *szFilename;
|
|
else
|
|
*safePos = '_';
|
|
|
|
++safePos;
|
|
++szFilename;
|
|
}
|
|
*safePos = 0;
|
|
szFilename = safeFilename;
|
|
|
|
// create temporary file
|
|
SCopy(Config.AtNetworkPath(GetFilename(szFilename)), pTarget, _MAX_PATH);
|
|
// file name is free?
|
|
if (!ItemExists(pTarget)) return true;
|
|
// find another file name
|
|
char szFileMask[_MAX_PATH+1];
|
|
SCopy(pTarget, szFileMask, GetExtension(pTarget)-1-pTarget);
|
|
SAppend("_%d", szFileMask, _MAX_PATH);
|
|
SAppend(GetExtension(pTarget)-1, szFileMask, _MAX_PATH);
|
|
for (int32_t i = 2; i < 1000; i++)
|
|
{
|
|
snprintf(pTarget, _MAX_PATH, szFileMask, i);
|
|
// doesn't exist?
|
|
if (!ItemExists(pTarget))
|
|
return true;
|
|
}
|
|
// not found
|
|
return false;
|
|
}
|