1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17#include <C4Include.h>
18#include <C4Network2Res.h>
19
20#include <C4Random.h>
21#include <C4Config.h>
22#include <C4Log.h>
23#include <C4Group.h>
24#include <C4Components.h>
25#include <C4Game.h>
26#include "StdAdaptors.h"
27
28#include <fcntl.h>
29#include <sys/types.h>
30#include <sys/stat.h>
31#ifdef _WIN32
32#include <direct.h>
33#endif
34#include <errno.h>
35
36// compile debug options
37// #define C4NET2RES_LOAD_ALL
38// #define C4NET2RES_DEBUG_LOG
39
40// helper
41
42class DirSizeHelper
43{
44 static uint32_t iSize, iMaxSize;
45 static bool Helper(const char *szPath)
46 {
47 if (szPath[SLen(sptr: szPath) - 1] == '.')
48 return false;
49 if (iSize > iMaxSize)
50 return false;
51 if (DirectoryExists(szFileName: szPath))
52 ForEachFile(szDirName: szPath, fnCallback: &Helper);
53 else if (FileExists(szFileName: szPath))
54 iSize += FileSize(fname: szPath);
55 return true;
56 }
57
58public:
59 static bool GetDirSize(const char *szPath, uint32_t *pSize, uint32_t inMaxSize = ~0)
60 {
61 // Security
62 if (!pSize) return false;
63 // Fold it
64 iSize = 0; iMaxSize = inMaxSize;
65 ForEachFile(szDirName: szPath, fnCallback: &Helper);
66 // Return
67 *pSize = iSize;
68 return true;
69 }
70};
71uint32_t DirSizeHelper::iSize, DirSizeHelper::iMaxSize;
72
73// *** C4Network2ResCore
74
75C4Network2ResCore::C4Network2ResCore()
76 : eType(NRT_Null),
77 iID(-1), iDerID(-1),
78 fLoadable(false),
79 iFileSize(~0u), iFileCRC(~0u), iContentsCRC(~0u),
80 iChunkSize(C4NetResChunkSize),
81 fHasFileSHA(false) {}
82
83void C4Network2ResCore::Set(C4Network2ResType enType, int32_t iResID, const char *strFileName, uint32_t inContentsCRC, const char *strAuthor)
84{
85 // Initialize base data
86 eType = enType; iID = iResID; iDerID = -1;
87 fLoadable = false;
88 iFileSize = iFileCRC = ~0; iContentsCRC = inContentsCRC;
89 iChunkSize = C4NetResChunkSize;
90 FileName.Copy(pnData: strFileName);
91 Author.Copy(pnData: strAuthor);
92}
93
94void C4Network2ResCore::SetLoadable(uint32_t iSize, uint32_t iCRC)
95{
96 fLoadable = true;
97 iFileSize = iSize;
98 iFileCRC = iCRC;
99}
100
101void C4Network2ResCore::Clear()
102{
103 eType = NRT_Null;
104 iID = iDerID = -1;
105 fLoadable = false;
106 FileName.Clear();
107 Author.Clear();
108 iFileSize = iFileCRC = iContentsCRC = ~0;
109 fHasFileSHA = false;
110}
111
112// C4PacketBase virtuals
113
114void C4Network2ResCore::CompileFunc(StdCompiler *pComp)
115{
116 constexpr StdEnumEntry<C4Network2ResType> C4Network2ResType_EnumMap[] =
117 {
118 { .Name: "Scenario", .Val: NRT_Scenario },
119 { .Name: "Dynamic", .Val: NRT_Dynamic },
120 { .Name: "Player", .Val: NRT_Player },
121 { .Name: "Definitions", .Val: NRT_Definitions },
122 { .Name: "System", .Val: NRT_System },
123 { .Name: "Material", .Val: NRT_Material },
124 };
125
126 pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eType, pNames: C4Network2ResType_EnumMap), szName: "Type", rDefault: NRT_Null));
127 pComp->Value(rStruct: mkNamingAdapt(rValue&: iID, szName: "ID", rDefault: -1));
128 pComp->Value(rStruct: mkNamingAdapt(rValue&: iDerID, szName: "DerID", rDefault: -1));
129 pComp->Value(rStruct: mkNamingAdapt(rValue&: fLoadable, szName: "Loadable", rDefault: true));
130 if (fLoadable)
131 {
132 pComp->Value(rStruct: mkNamingAdapt(rValue&: iFileSize, szName: "FileSize", rDefault: 0U));
133 pComp->Value(rStruct: mkNamingAdapt(rValue&: iFileCRC, szName: "FileCRC", rDefault: 0U));
134 pComp->Value(rStruct: mkNamingAdapt(rValue&: iChunkSize, szName: "ChunkSize", rDefault: C4NetResChunkSize));
135 if (!iChunkSize) pComp->excCorrupt(message: "zero chunk size");
136 }
137 pComp->Value(rStruct: mkNamingAdapt(rValue&: iContentsCRC, szName: "ContentsCRC", rDefault: 0U));
138 pComp->Value(rStruct: mkNamingCountAdapt(iCount&: fHasFileSHA, szName: "FileSHA"));
139 if (fHasFileSHA)
140 pComp->Value(rStruct: mkNamingAdapt(rValue: mkHexAdapt(rData&: FileSHA), szName: "FileSHA"));
141 pComp->Value(rStruct: mkNamingAdapt(rValue: mkNetFilenameAdapt(FileName), szName: "Filename", rDefault: ""));
142 pComp->Value(rStruct: mkNamingAdapt(rValue: mkNetFilenameAdapt(FileName&: Author), szName: "Author", rDefault: ""));
143}
144
145// *** C4Network2ResLoad
146
147C4Network2ResLoad::C4Network2ResLoad(int32_t inChunk, int32_t inByClient)
148 : iChunk(inChunk), iByClient(inByClient), Timestamp(time(timer: nullptr)), pNext(nullptr) {}
149
150C4Network2ResLoad::~C4Network2ResLoad() {}
151
152bool C4Network2ResLoad::CheckTimeout()
153{
154 return difftime(time1: time(timer: nullptr), time0: Timestamp) >= C4NetResLoadTimeout;
155}
156
157// *** C4Network2ResChunkData
158
159C4Network2ResChunkData::C4Network2ResChunkData()
160 : iChunkCnt(0), iPresentChunkCnt(0),
161 pChunkRanges(nullptr), iChunkRangeCnt(0) {}
162
163C4Network2ResChunkData::C4Network2ResChunkData(const C4Network2ResChunkData &Data2)
164 : C4PacketBase(Data2),
165 iChunkCnt(Data2.getChunkCnt()), iPresentChunkCnt(0),
166 pChunkRanges(nullptr), iChunkRangeCnt(0)
167{
168 // add ranges
169 Merge(Data2);
170}
171
172C4Network2ResChunkData::~C4Network2ResChunkData()
173{
174 Clear();
175}
176
177C4Network2ResChunkData &C4Network2ResChunkData::operator=(const C4Network2ResChunkData &Data2)
178{
179 // clear, merge
180 SetIncomplete(Data2.getChunkCnt());
181 Merge(Data2);
182 return *this;
183}
184
185void C4Network2ResChunkData::SetIncomplete(int32_t inChunkCnt)
186{
187 Clear();
188 // just set total chunk count
189 iChunkCnt = inChunkCnt;
190}
191
192void C4Network2ResChunkData::SetComplete(int32_t inChunkCnt)
193{
194 Clear();
195 // set total chunk count
196 iPresentChunkCnt = iChunkCnt = inChunkCnt;
197 // create one range
198 ChunkRange *pRange = new ChunkRange;
199 pRange->Start = 0; pRange->Length = iChunkCnt;
200 pRange->Next = nullptr;
201 pChunkRanges = pRange;
202}
203
204void C4Network2ResChunkData::AddChunk(int32_t iChunk)
205{
206 AddChunkRange(iStart: iChunk, iLength: 1);
207}
208
209void C4Network2ResChunkData::AddChunkRange(int32_t iStart, int32_t iLength)
210{
211 // security
212 if (iStart < 0 || iStart + iLength > iChunkCnt || iLength <= 0) return;
213 // find position
214 ChunkRange *pRange, *pPrev;
215 for (pRange = pChunkRanges, pPrev = nullptr; pRange; pPrev = pRange, pRange = pRange->Next)
216 if (pRange->Start >= iStart)
217 break;
218 // create new
219 ChunkRange *pNew = new ChunkRange;
220 pNew->Start = iStart; pNew->Length = iLength;
221 // add to list
222 pNew->Next = pRange;
223 (pPrev ? pPrev->Next : pChunkRanges) = pNew;
224 // counts
225 iPresentChunkCnt += iLength; iChunkRangeCnt++;
226 // check merges
227 if (pPrev && MergeRanges(pRange: pPrev))
228 while (MergeRanges(pRange: pPrev));
229 else
230 while (MergeRanges(pRange: pNew));
231}
232
233void C4Network2ResChunkData::Merge(const C4Network2ResChunkData &Data2)
234{
235 // must have same basis chunk count
236 assert(iChunkCnt == Data2.getChunkCnt());
237 // add ranges
238 for (ChunkRange *pRange = Data2.pChunkRanges; pRange; pRange = pRange->Next)
239 AddChunkRange(iStart: pRange->Start, iLength: pRange->Length);
240}
241
242void C4Network2ResChunkData::Clear()
243{
244 iChunkCnt = iPresentChunkCnt = iChunkRangeCnt = 0;
245 // remove all ranges
246 while (pChunkRanges)
247 {
248 ChunkRange *pDelete = pChunkRanges;
249 pChunkRanges = pDelete->Next;
250 delete pDelete;
251 }
252}
253
254int32_t C4Network2ResChunkData::GetChunkToRetrieve(const C4Network2ResChunkData &Available, int32_t iLoadingCnt, int32_t *pLoading) const
255{
256 // (this version is highly calculation-intensitive, yet the most satisfactory
257 // solution I could find)
258
259 // find everything that should not be retrieved
260 C4Network2ResChunkData ChData; Available.GetNegative(Target&: ChData);
261 ChData.Merge(Data2: *this);
262 for (int32_t i = 0; i < iLoadingCnt; i++)
263 ChData.AddChunk(iChunk: pLoading[i]);
264 // nothing to retrieve?
265 if (ChData.isComplete()) return -1;
266 // invert to get everything that should be retrieved
267 C4Network2ResChunkData ChData2; ChData.GetNegative(Target&: ChData2);
268 // select chunk (random)
269 int32_t iRetrieveChunk = SafeRandom(range: ChData2.getPresentChunkCnt());
270 // return
271 return ChData2.getPresentChunk(iNr: iRetrieveChunk);
272}
273
274bool C4Network2ResChunkData::MergeRanges(ChunkRange *pRange)
275{
276 // no next entry?
277 if (!pRange || !pRange->Next) return false;
278 // do merge?
279 ChunkRange *pNext = pRange->Next;
280 if (pRange->Start + pRange->Length < pNext->Start) return false;
281 // get overlap
282 int32_t iOverlap = (std::min)(a: (pRange->Start + pRange->Length) - pNext->Start, b: pNext->Length);
283 // set new chunk range
284 pRange->Length += pNext->Length - iOverlap;
285 // remove range
286 pRange->Next = pNext->Next;
287 delete pNext;
288 // counts
289 iChunkRangeCnt--; iPresentChunkCnt -= iOverlap;
290 // ok
291 return true;
292}
293
294void C4Network2ResChunkData::GetNegative(C4Network2ResChunkData &Target) const
295{
296 // clear target
297 Target.SetIncomplete(iChunkCnt);
298 // add all ranges that are missing
299 int32_t iFreeStart = 0;
300 for (ChunkRange *pRange = pChunkRanges; pRange; pRange = pRange->Next)
301 {
302 // add range
303 Target.AddChunkRange(iStart: iFreeStart, iLength: pRange->Start - iFreeStart);
304 // safe new start
305 iFreeStart = pRange->Start + pRange->Length;
306 }
307 // add last range
308 Target.AddChunkRange(iStart: iFreeStart, iLength: iChunkCnt - iFreeStart);
309}
310
311int32_t C4Network2ResChunkData::getPresentChunk(int32_t iNr) const
312{
313 for (ChunkRange *pRange = pChunkRanges; pRange; pRange = pRange->Next)
314 if (iNr < pRange->Length)
315 return iNr + pRange->Start;
316 else
317 iNr -= pRange->Length;
318 return -1;
319}
320
321void C4Network2ResChunkData::CompileFunc(StdCompiler *pComp)
322{
323 bool fCompiler = pComp->isCompiler();
324 if (fCompiler) Clear();
325 // Data
326 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iChunkCnt), szName: "ChunkCnt", rDefault: 0));
327 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iChunkRangeCnt), szName: "ChunkRangeCnt", rDefault: 0));
328 const auto name = pComp->Name(szName: "Ranges");
329 // Ranges
330 if (!name)
331 pComp->excCorrupt(message: "ResChunk ranges expected!");
332 ChunkRange *pRange = nullptr;
333 for (int32_t i = 0; i < iChunkRangeCnt; i++)
334 {
335 // Create new range / go to next range
336 if (fCompiler)
337 pRange = (pRange ? pRange->Next : pChunkRanges) = new ChunkRange;
338 else
339 pRange = pRange ? pRange->Next : pChunkRanges;
340 // Separate
341 if (i) pComp->Separator();
342 // Compile range
343 pComp->Value(rStruct: mkIntPackAdapt(rVal&: pRange->Start));
344 pComp->Separator(eSep: StdCompiler::SEP_PART2);
345 pComp->Value(rStruct: mkIntPackAdapt(rVal&: pRange->Length));
346 }
347 // Terminate list
348 if (fCompiler)
349 (pRange ? pRange->Next : pChunkRanges) = nullptr;
350}
351
352// *** C4Network2Res
353
354C4Network2Res::C4Network2Res(C4Network2ResList *pnParent)
355 : fDirty(false),
356 fTempFile(false), fStandaloneFailed(false),
357 iRefCnt(0), fRemoved(false),
358 iLastReqTime(0),
359 fLoading(false),
360 pCChunks(nullptr), iDiscoverStartTime(0), pLoads(nullptr), iLoadCnt(0),
361 pNext(nullptr),
362 pParent(pnParent)
363{
364 szFile[0] = szStandalone[0] = '\0';
365}
366
367C4Network2Res::~C4Network2Res()
368{
369 assert(!pNext);
370 Clear();
371}
372
373bool C4Network2Res::SetByFile(const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fSilent)
374{
375 CStdLock FileLock(&FileCSec);
376 // default ressource name: relative path
377 if (!szResName) szResName = Config.AtExeRelativePath(szFilename: strFilePath);
378 SCopy(szSource: strFilePath, sTarget: szFile, iMaxL: sizeof(szFile) - 1);
379 // group?
380 C4Group Grp;
381 if (Grp.Open(szGroupName: strFilePath))
382 return SetByGroup(pGrp: &Grp, fTemp, eType, iResID, szResName, fSilent);
383 // so it needs to be a file
384 if (!FileExists(szFileName: szFile))
385 {
386 if (!fSilent) pParent->logger->error(fmt: "SetByFile: file {} not found!", args&: strFilePath); return false;
387 }
388 // calc checksum
389 uint32_t iCRC32;
390 if (!C4Group_GetFileCRC(szFilename: szFile, pCRC32: &iCRC32)) return false;
391#ifdef C4NET2RES_DEBUG_LOG
392 // log
393 pParent->logger->trace("Resource: complete {}:{} is file {} ({})", iResID, szResName, szFile, fTemp ? "temp" : "static");
394#endif
395 // set core
396 Core.Set(enType: eType, iResID, strFileName: szResName, inContentsCRC: iCRC32, strAuthor: "");
397 // set own data
398 fDirty = true;
399 fTempFile = fTemp;
400 fStandaloneFailed = false;
401 fRemoved = false;
402 iLastReqTime = time(timer: nullptr);
403 fLoading = false;
404 local = true;
405 // ok
406 return true;
407}
408
409bool C4Network2Res::SetByGroup(C4Group *pGrp, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fSilent) // by main thread
410{
411 Clear();
412 CStdLock FileLock(&FileCSec);
413 // default ressource name: relative path
414 StdStrBuf sResName;
415 if (szResName)
416 sResName.Ref(pnData: szResName);
417 else
418 {
419 StdStrBuf sFullName = pGrp->GetFullName();
420 sResName.Copy(pnData: Config.AtExeRelativePath(szFilename: sFullName.getData()));
421 }
422 SCopy(szSource: pGrp->GetFullName().getData(), sTarget: szFile, iMaxL: sizeof(szFile) - 1);
423 // set core
424 Core.Set(enType: eType, iResID, strFileName: sResName.getData(), inContentsCRC: pGrp->EntryCRC32(), strAuthor: pGrp->GetMaker());
425#ifdef C4NET2RES_DEBUG_LOG
426 // log
427 pParent->logger->trace("Resource: complete {}:{} is file {} ({})", iResID, sResName.getData(), szFile, fTemp ? "temp" : "static");
428#endif
429 // set data
430 fDirty = true;
431 fTempFile = fTemp;
432 fStandaloneFailed = false;
433 fRemoved = false;
434 iLastReqTime = time(timer: nullptr);
435 fLoading = false;
436 local = true;
437 // ok
438 return true;
439}
440
441bool C4Network2Res::SetByCore(const C4Network2ResCore &nCore, bool fSilent, const char *szAsFilename, int32_t iRecursion) // by main thread
442{
443 // try open local file
444 const char *szFilename = szAsFilename ? szAsFilename : GetC4Filename(szPath: nCore.getFileName());
445 if (SetByFile(strFilePath: szFilename, fTemp: false, eType: nCore.getType(), iResID: nCore.getID(), szResName: nCore.getFileName(), fSilent))
446 {
447 // check contents checksum
448 if (Core.getContentsCRC() == nCore.getContentsCRC())
449 {
450 // set core
451 fDirty = true;
452 Core = nCore;
453
454 // to ensure correct file sorting
455 GetStandalone(pTo: nullptr, iMaxL: 0, fSetOfficial: false, fAllowUnloadable: false, fSilent: false);
456 // ok then
457 return true;
458 }
459 }
460 // get and search for filename without specified folder (e.g., Castle.c4s when the opened game is Easy.c4f\Castle.c4s)
461 const char *szFilenameOnly = GetFilename(path: szFilename);
462 const char *szFilenameC4 = GetC4Filename(szPath: szFilename);
463 if (szFilenameOnly != szFilenameC4)
464 {
465 if (SetByCore(nCore, fSilent, szAsFilename: szFilenameOnly, iRecursion: Config.Network.MaxResSearchRecursion)) return true;
466 }
467 // if it could still not be set, try within all folders of root (ignoring special folders), and try as file outside the folder
468 // but do not recurse any deeper than set by config (default: One folder)
469 if (iRecursion >= Config.Network.MaxResSearchRecursion) return false;
470 StdStrBuf sSearchPath; const char *szSearchPath;
471 if (!iRecursion)
472 szSearchPath = Config.General.ExePath;
473 else
474 {
475 sSearchPath.Copy(pnData: szFilename, iChars: SLen(sptr: szFilename) - SLen(sptr: szFilenameC4));
476 szSearchPath = sSearchPath.getData();
477 }
478 StdStrBuf sNetPath; sNetPath.Copy(pnData: Config.Network.WorkPath);
479 char *szNetPath = sNetPath.GrabPointer();
480 TruncateBackslash(szFilename: szNetPath);
481 sNetPath.Take(pnData: szNetPath);
482 for (DirectoryIterator i(szSearchPath); *i; ++i)
483 if (DirectoryExists(szFileName: *i))
484 if (!*GetExtension(fname: *i)) // directories without extension only
485 if (!szNetPath || !*szNetPath || !ItemIdentical(szFilename1: *i, szFilename2: szNetPath)) // ignore network path
486 {
487 // search for complete name at subpath (e.g. MyFolder\Easy.c4f\Castle.c4s)
488 const std::string filename{std::format(fmt: "{}" DirSep "{}", args: *i, args&: szFilenameC4)};
489 if (SetByCore(nCore, fSilent, szAsFilename: filename.c_str(), iRecursion: iRecursion + 1))
490 return true;
491 }
492 // file could not be found locally
493 return false;
494}
495
496bool C4Network2Res::SetLoad(const C4Network2ResCore &nCore) // by main thread
497{
498 Clear();
499 CStdLock FileLock(&FileCSec);
500 // must be loadable
501 if (!nCore.isLoadable()) return false;
502 // save core, set chunks
503 Core = nCore;
504 Chunks.SetIncomplete(Core.getChunkCnt());
505 // create temporary file
506 if (!pParent->FindTempResFileName(szFilename: Core.getFileName(), pTarget: szFile))
507 return false;
508#ifdef C4NET2RES_DEBUG_LOG
509 // log
510 pParent->logger->trace("Resource: loading {}:{} to file {}", Core.getID(), Core.getFileName(), szFile);
511#endif
512 // set standalone (result is going to be binary-compatible)
513 SCopy(szSource: szFile, sTarget: szStandalone, iMaxL: sizeof(szStandalone) - 1);
514 // set flags
515 fDirty = false;
516 fTempFile = true;
517 fStandaloneFailed = false;
518 fRemoved = false;
519 iLastReqTime = time(timer: nullptr);
520 fLoading = true;
521 // No discovery yet
522 iDiscoverStartTime = 0;
523 return true;
524}
525
526bool C4Network2Res::SetDerived(const char *strName, const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iDResID)
527{
528 Clear();
529 CStdLock FileLock(&FileCSec);
530 // set core
531 Core.Set(enType: eType, iResID: C4NetResIDAnonymous, strFileName: strName, inContentsCRC: ~0, strAuthor: "");
532 Core.SetDerived(iDResID);
533 // save file path
534 SCopy(szSource: strFilePath, sTarget: szFile, _MAX_PATH);
535 *szStandalone = '\0';
536 // set flags
537 fDirty = false;
538 fTempFile = fTemp;
539 fStandaloneFailed = false;
540 fRemoved = false;
541 iLastReqTime = time(timer: nullptr);
542 fLoading = false;
543 // Do not set any chunk data - anonymous ressources are very likely to change.
544 // Wait for FinishDerived()-call.
545 return true;
546}
547
548void C4Network2Res::ChangeID(int32_t inID)
549{
550 Core.SetID(inID);
551}
552
553bool C4Network2Res::IsBinaryCompatible()
554{
555 // returns wether the standalone of this ressource is binary compatible
556 // to the official version (means: matches the file checksum)
557
558 CStdLock FileLock(&FileCSec);
559 // standalone set? ok then (see GetStandalone)
560 if (szStandalone[0]) return true;
561 // is a directory?
562 if (DirectoryExists(szFileName: szFile))
563 // forget it - if the file is packed now, the creation time and author
564 // won't match.
565 return false;
566 // try to create the standalone
567 return GetStandalone(pTo: nullptr, iMaxL: 0, fSetOfficial: false, fAllowUnloadable: false, fSilent: true);
568}
569
570bool C4Network2Res::GetStandalone(char *pTo, int32_t iMaxL, bool fSetOfficial, bool fAllowUnloadable, bool fSilent)
571{
572 // already set?
573 if (szStandalone[0])
574 {
575 if (pTo) SCopy(szSource: szStandalone, sTarget: pTo, iMaxL);
576 return true;
577 }
578 // already tried and failed? No point in retrying
579 if (fStandaloneFailed) return false;
580 // not loadable? Wo won't be able to check the standalone as the core will lack the needed information.
581 // the standalone won't be interesting in this case, anyway.
582 if (!fSetOfficial && !Core.isLoadable()) return false;
583 // set flag, so failure below will let future calls fail
584 fStandaloneFailed = true;
585 // lock file
586 CStdLock FileLock(&FileCSec);
587
588 // directory?
589 SCopy(szSource: szFile, sTarget: szStandalone, iMaxL: sizeof(szStandalone) - 1);
590 if (DirectoryExists(szFileName: szFile))
591 {
592 // size check for the directory, if allowed
593 if (fAllowUnloadable)
594 {
595 uint32_t iDirSize;
596 if (!DirSizeHelper::GetDirSize(szPath: szFile, pSize: &iDirSize, inMaxSize: Config.Network.MaxLoadFileSize))
597 {
598 if (!fSilent) pParent->logger->error(fmt: "could not get directory size of {}!", args&: szFile); szStandalone[0] = '\0'; return false;
599 }
600 if (iDirSize > uint32_t(Config.Network.MaxLoadFileSize))
601 {
602 if (!fSilent) pParent->logger->error(fmt: "{} over size limit, will be marked unloadable!", args&: szFile); szStandalone[0] = '\0'; return false;
603 }
604 }
605 // log - this may take a few seconds
606 if (!fSilent) Log(id: C4ResStrTableKey::IDS_PRC_NETPACKING, args: GetFilename(path: szFile));
607 // pack inplace?
608 if (!fTempFile)
609 {
610 if (!pParent->FindTempResFileName(szFilename: szFile, pTarget: szStandalone))
611 {
612 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not find free name for temporary file!"); szStandalone[0] = '\0'; return false;
613 }
614 if (!C4Group_PackDirectoryTo(szFilename: szFile, szFilenameTo: szStandalone, overwriteTarget: true))
615 {
616 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not pack directory!"); szStandalone[0] = '\0'; return false;
617 }
618 }
619 else if (!C4Group_PackDirectory(szFilename: szStandalone))
620 {
621 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not pack directory!"); if (!SEqual(szStr1: szFile, szStr2: szStandalone)) EraseDirectory(szDirName: szStandalone); szStandalone[0] = '\0'; return false;
622 }
623 // make sure directory is packed
624 if (DirectoryExists(szFileName: szStandalone))
625 {
626 if (!fSilent) pParent->logger->error(msg: "GetStandalone: directory hasn't been packed!"); if (!SEqual(szStr1: szFile, szStr2: szStandalone)) EraseDirectory(szDirName: szStandalone); szStandalone[0] = '\0'; return false;
627 }
628 strcpy(dest: szFile, src: szStandalone);
629 fTempFile = true;
630 // fallthru
631 }
632
633 // doesn't exist physically?
634 if (!FileExists(szFileName: szStandalone))
635 {
636 // try C4Group (might be packed)
637 if (!pParent->FindTempResFileName(szFilename: szFile, pTarget: szStandalone))
638 {
639 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not find free name for temporary file!"); szStandalone[0] = '\0'; return false;
640 }
641 if (!C4Group_CopyItem(szSource: szFile, szTarget: szStandalone))
642 {
643 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not copy to temporary file!"); szStandalone[0] = '\0'; return false;
644 }
645 }
646
647 // remains missing? give up.
648 if (!FileExists(szFileName: szStandalone))
649 {
650 if (!fSilent) pParent->logger->error(msg: "GetStandalone: file not found!"); szStandalone[0] = '\0'; return false;
651 }
652
653 // do optimizations (delete unneeded entries)
654 if (!OptimizeStandalone(fSilent))
655 {
656 if (!SEqual(szStr1: szFile, szStr2: szStandalone)) remove(filename: szStandalone); szStandalone[0] = '\0'; return false;
657 }
658
659 // get file size
660 size_t iSize = FileSize(fname: szStandalone);
661 // size limit
662 if (fAllowUnloadable)
663 if (iSize > uint32_t(Config.Network.MaxLoadFileSize))
664 {
665 if (!fSilent) pParent->logger->info(fmt: "{} over size limit, will be marked unloadable!", args&: szFile); szStandalone[0] = '\0'; return false;
666 }
667 // check
668 if (!fSetOfficial && iSize != Core.getFileSize())
669 {
670 // remove file
671 if (!SEqual(szStr1: szFile, szStr2: szStandalone)) remove(filename: szStandalone); szStandalone[0] = '\0';
672 // sorry, this version isn't good enough :(
673 return false;
674 }
675
676 // calc checksum
677 uint32_t iCRC32;
678 if (!C4Group_GetFileCRC(szFilename: szStandalone, pCRC32: &iCRC32))
679 {
680 if (!fSilent) pParent->logger->error(msg: "GetStandalone: could not calculate checksum!"); return false;
681 }
682 // set / check
683 if (!fSetOfficial && iCRC32 != Core.getFileCRC())
684 {
685 // remove file, return
686 if (!SEqual(szStr1: szFile, szStr2: szStandalone)) remove(filename: szStandalone); szStandalone[0] = '\0';
687 return false;
688 }
689
690 // we didn't fail
691 fStandaloneFailed = false;
692 // mark resource as loadable and safe file information
693 Core.SetLoadable(iSize, iCRC: iCRC32);
694 // set up chunk data
695 Chunks.SetComplete(Core.getChunkCnt());
696 // ok
697 return true;
698}
699
700bool C4Network2Res::CalculateSHA()
701{
702 // already present?
703 if (Core.hasFileSHA()) return true;
704 // get the file
705 char szStandalone[_MAX_PATH + 1];
706 if (!GetStandalone(pTo: szStandalone, _MAX_PATH, fSetOfficial: false))
707 SCopy(szSource: szFile, sTarget: szStandalone, _MAX_PATH);
708 // get the hash
709 uint8_t hash[StdSha1::DigestLength];
710 if (!C4Group_GetFileSHA1(szFilename: szStandalone, pSHA1: hash))
711 return false;
712 // save it back
713 Core.SetFileSHA(hash);
714 // okay
715 return true;
716}
717
718C4Network2Res::Ref C4Network2Res::Derive()
719{
720 // Called before the file is changed. Rescues all files and creates a
721 // new ressource for the file. This ressource is flagged as "anonymous", as it
722 // has no official core (no res ID, to be exact).
723 // The resource gets its final core when FinishDerive() is called.
724
725 // For security: This doesn't make much sense if the resource is currently being
726 // loaded. So better assume the caller doesn't know what he's doing and check.
727 if (isLoading()) return nullptr;
728
729 CStdLock FileLock(&FileCSec);
730 // Save back original file name
731 char szOrgFile[_MAX_PATH + 1];
732 SCopy(szSource: szFile, sTarget: szOrgFile, _MAX_PATH);
733 bool fOrgTempFile = fTempFile;
734
735 // Create a copy of the file, if neccessary
736 if (!*szStandalone || SEqual(szStr1: szStandalone, szStr2: szFile))
737 {
738 if (!pParent->FindTempResFileName(szFilename: szOrgFile, pTarget: szFile))
739 {
740 pParent->logger->error(msg: "Derive: could not find free name for temporary file!"); return nullptr;
741 }
742 if (!C4Group_CopyItem(szSource: szOrgFile, szTarget: szFile))
743 {
744 pParent->logger->error(msg: "Derive: could not copy to temporary file!"); return nullptr;
745 }
746 // set standalone
747 if (*szStandalone)
748 SCopy(szSource: szFile, sTarget: szStandalone, _MAX_PATH);
749 fTempFile = true;
750 }
751 else
752 {
753 // Standlone exists: Just set szFile to point on the standlone. It's
754 // assumed that the original file isn't of intrest after this point anyway.
755 SCopy(szSource: szStandalone, sTarget: szFile, _MAX_PATH);
756 fTempFile = true;
757 }
758
759 pParent->logger->info(fmt: "Resource: deriving from {}:{}, original at {}", args: getResID(), args: Core.getFileName(), args&: szFile);
760
761 // (note: should remove temp file if something fails after this point)
762
763 // create new ressource
764 C4Network2Res::Ref pDRes = new C4Network2Res(pParent);
765 if (!pDRes) return nullptr;
766
767 // initialize
768 if (!pDRes->SetDerived(strName: Core.getFileName(), strFilePath: szOrgFile, fTemp: fOrgTempFile, eType: getType(), iDResID: getResID()))
769 return nullptr;
770
771 // add to list
772 pParent->Add(pRes: pDRes);
773
774 // return new ressource
775 return pDRes;
776}
777
778bool C4Network2Res::FinishDerive() // by main thread
779{
780 // All changes have been made. Register this ressource with a new ID.
781
782 // security
783 if (!isAnonymous()) return false;
784
785 CStdLock FileLock(&FileCSec);
786 // Save back data
787 int32_t iDerID = Core.getDerID();
788 char szName[_MAX_PATH + 1]; SCopy(szSource: Core.getFileName(), sTarget: szName, _MAX_PATH);
789 char szFileC[_MAX_PATH + 1]; SCopy(szSource: szFile, sTarget: szFileC, _MAX_PATH);
790 // Set by file
791 if (!SetByFile(strFilePath: szFileC, fTemp: fTempFile, eType: getType(), iResID: pParent->nextResID(), szResName: szName))
792 return false;
793 // create standalone
794 if (!GetStandalone(pTo: nullptr, iMaxL: 0, fSetOfficial: true))
795 return false;
796 // Set ID
797 Core.SetDerived(iDerID);
798 // announce derive
799 pParent->getIOClass()->BroadcastMsg(rPkt: MkC4NetIOPacket(cStatus: PID_NetResDerive, Pkt: Core));
800 // derivation is dirty bussines
801 fDirty = true;
802 // ok
803 return true;
804}
805
806bool C4Network2Res::FinishDerive(const C4Network2ResCore &nCore)
807{
808 // security
809 if (!isAnonymous()) return false;
810 // Set core
811 Core = nCore;
812 // Set chunks (assume the ressource is complete)
813 Chunks.SetComplete(Core.getChunkCnt());
814
815 // Note that the Contents-CRC is /not/ checked. Derivation needs to be
816 // synchronized outside of C4Network2Res.
817
818 // But note that the ressource /might/ be binary compatible (though very
819 // unlikely), so do not set fNotBinaryCompatible.
820
821 // ok
822 return true;
823}
824
825void C4Network2Res::Remove()
826{
827 // schedule for removal
828 fRemoved = true;
829}
830
831bool C4Network2Res::SendStatus(C4Network2IOConnection *pTo)
832{
833 // pack status
834 C4NetIOPacket Pkt = MkC4NetIOPacket(cStatus: PID_NetResStat, Pkt: C4PacketResStatus(Core.getID(), Chunks));
835 // to one client?
836 if (pTo)
837 return pTo->Send(rPkt: Pkt);
838 else
839 {
840 // reset dirty flag
841 fDirty = false;
842 // broadcast status
843 assert(pParent && pParent->getIOClass());
844 return pParent->getIOClass()->BroadcastMsg(rPkt: Pkt);
845 }
846}
847
848bool C4Network2Res::SendChunk(uint32_t iChunk, int32_t iToClient)
849{
850 assert(pParent && pParent->getIOClass());
851 if (!szStandalone[0] || iChunk >= Core.getChunkCnt()) return false;
852 // find connection for given client (one of the rare uses of the data connection)
853 C4Network2IOConnection *pConn = pParent->getIOClass()->GetDataConnection(iClientID: iToClient);
854 if (!pConn) return false;
855 // save last request time
856 iLastReqTime = time(timer: nullptr);
857 // create packet
858 CStdLock FileLock(&FileCSec);
859 C4Network2ResChunk ResChunk;
860 ResChunk.Set(pRes: this, iChunk);
861 // send
862 bool fSuccess = pConn->Send(rPkt: MkC4NetIOPacket(cStatus: PID_NetResData, Pkt: ResChunk));
863 pConn->DelRef();
864 return fSuccess;
865}
866
867void C4Network2Res::AddRef()
868{
869 ++iRefCnt;
870}
871
872void C4Network2Res::DelRef()
873{
874 if (--iRefCnt == 0) delete this;
875}
876
877void C4Network2Res::OnDiscover(C4Network2IOConnection *pBy)
878{
879 if (!IsBinaryCompatible()) return;
880 // discovered
881 iLastReqTime = time(timer: nullptr);
882 // send status back
883 SendStatus(pTo: pBy);
884}
885
886void C4Network2Res::OnStatus(const C4Network2ResChunkData &rChunkData, C4Network2IOConnection *pBy)
887{
888 // discovered a source: reset timeout
889 iDiscoverStartTime = 0;
890 // check if the chunk data is valid
891 if (rChunkData.getChunkCnt() != Chunks.getChunkCnt())
892 return;
893 // add chunk data
894 ClientChunks *pChunks;
895 for (pChunks = pCChunks; pChunks; pChunks = pChunks->Next)
896 if (pChunks->ClientID == pBy->getClientID())
897 break;
898 // not found? add
899 if (!pChunks)
900 {
901 pChunks = new ClientChunks();
902 pChunks->Next = pCChunks;
903 pCChunks = pChunks;
904 }
905 pChunks->ClientID = pBy->getClientID();
906 pChunks->Chunks = rChunkData;
907 // load?
908 if (fLoading) StartLoad(iFromClient: pChunks->ClientID, Chunks: pChunks->Chunks);
909}
910
911void C4Network2Res::OnChunk(const C4Network2ResChunk &rChunk)
912{
913 if (!fLoading) return;
914 // correct ressource?
915 if (rChunk.getResID() != getResID()) return;
916 // add ressource data
917 CStdLock FileLock(&FileCSec);
918 bool fSuccess = rChunk.AddTo(pRes: this, pIO: pParent->getIOClass());
919#ifdef C4NET2RES_DEBUG_LOG
920 // log
921 pParent->logger->trace("Res: {} chunk {} to resource {} ({}){}", fSuccess ? "added" : "could not add", rChunk.getChunkNr(), Core.getFileName(), szFile, fSuccess ? "" : "!");
922#endif
923 if (fSuccess)
924 {
925 // status changed
926 fDirty = true;
927 // remove load waits
928 for (C4Network2ResLoad *pLoad = pLoads, *pNext, *pPrev = nullptr; pLoad; pPrev = pLoad, pLoad = pNext)
929 {
930 pNext = pLoad->Next();
931 if (pLoad->getChunk() == rChunk.getChunkNr())
932 RemoveLoad(pLoad);
933 }
934 }
935 // complete?
936 if (Chunks.isComplete())
937 EndLoad();
938 // check: start new loads?
939 else
940 StartNewLoads();
941}
942
943bool C4Network2Res::DoLoad()
944{
945 if (!fLoading) return true;
946 // any loads currently active?
947 if (iLoadCnt)
948 {
949 // check for load timeouts
950 int32_t iLoadsRemoved = 0;
951 for (C4Network2ResLoad *pLoad = pLoads, *pNext; pLoad; pLoad = pNext)
952 {
953 pNext = pLoad->Next();
954 if (pLoad->CheckTimeout())
955 {
956 RemoveLoad(pLoad);
957 iLoadsRemoved++;
958 }
959 }
960 // start new loads
961 if (iLoadsRemoved) StartNewLoads();
962 }
963 else
964 {
965 // discover timeout?
966 if (iDiscoverStartTime)
967 if (difftime(time1: time(timer: nullptr), time0: iDiscoverStartTime) > C4NetResDiscoverTimeout)
968 return false;
969 }
970 // ok
971 return true;
972}
973
974bool C4Network2Res::NeedsDiscover()
975{
976 // set timeout, if this is the first discover
977 if (!iDiscoverStartTime)
978 iDiscoverStartTime = time(timer: nullptr);
979 // do discover
980 return true;
981}
982
983void C4Network2Res::Clear()
984{
985 CStdLock FileLock(&FileCSec);
986 // delete files
987 if (fTempFile)
988 if (FileExists(szFileName: szFile))
989 if (remove(filename: szFile))
990 pParent->logger->error(fmt: "Could not delete temporary resource file ({})", args: strerror(errno));
991 if (szStandalone[0] && !SEqual(szStr1: szFile, szStr2: szStandalone))
992 if (FileExists(szFileName: szStandalone))
993 if (remove(filename: szStandalone))
994 pParent->logger->error(fmt: "Could not delete temporary resource file ({})", args: strerror(errno));
995 szFile[0] = szStandalone[0] = '\0';
996 fDirty = false;
997 fTempFile = false;
998 Core.Clear();
999 Chunks.Clear();
1000 fRemoved = false;
1001 ClearLoad();
1002}
1003
1004int32_t C4Network2Res::OpenFileRead()
1005{
1006 CStdLock FileLock(&FileCSec);
1007 if (!GetStandalone(pTo: nullptr, iMaxL: 0, fSetOfficial: false, fAllowUnloadable: false, fSilent: true)) return -1;
1008 return open(file: szStandalone, _O_BINARY | O_RDONLY);
1009}
1010
1011int32_t C4Network2Res::OpenFileWrite()
1012{
1013 CStdLock FileLock(&FileCSec);
1014 return open(file: szStandalone, _O_BINARY | O_CREAT | O_WRONLY, S_IREAD | S_IWRITE);
1015}
1016
1017void C4Network2Res::StartNewLoads()
1018{
1019 if (!pCChunks) return;
1020 // count clients
1021 int32_t iCChunkCnt = 0; ClientChunks *pChunks;
1022 for (pChunks = pCChunks; pChunks; pChunks = pChunks->Next)
1023 iCChunkCnt++;
1024 // create array
1025 ClientChunks **pC = new ClientChunks *[iCChunkCnt];
1026 // initialize
1027 int32_t i;
1028 for (i = 0; i < iCChunkCnt; i++) pC[i] = nullptr;
1029 // create shuffled order
1030 for (pChunks = pCChunks, i = 0; i < iCChunkCnt; i++, pChunks = pChunks->Next)
1031 {
1032 // determine position
1033 int32_t iPos = SafeRandom(range: iCChunkCnt - i);
1034 // find & set
1035 for (int32_t j = 0; ; j++)
1036 if (!pC[j] && !iPos--)
1037 {
1038 pC[j] = pChunks;
1039 break;
1040 }
1041 }
1042 // start new load until maximum count reached
1043 while (iLoadCnt + 1 <= C4NetResMaxLoad)
1044 {
1045 int32_t ioLoadCnt = iLoadCnt;
1046 // search someone
1047 for (i = 0; i < iCChunkCnt; i++)
1048 if (pC[i])
1049 {
1050 // try to start load
1051 if (!StartLoad(iFromClient: pC[i]->ClientID, Chunks: pC[i]->Chunks))
1052 {
1053 pC[i] = nullptr; continue;
1054 }
1055 // success?
1056 if (iLoadCnt > ioLoadCnt) break;
1057 }
1058 // not found?
1059 if (i >= iCChunkCnt)
1060 break;
1061 }
1062 // clear up
1063 delete[] pC;
1064}
1065
1066bool C4Network2Res::StartLoad(int32_t iFromClient, const C4Network2ResChunkData &Available)
1067{
1068 assert(pParent && pParent->getIOClass());
1069 // all slots used? ignore
1070 if (iLoadCnt + 1 >= C4NetResMaxLoad) return true;
1071 int32_t loadsAtClient = 0;
1072 // are there already enough loads by this client? ignore
1073 for (C4Network2ResLoad *pPos = pLoads; pPos; pPos = pPos->Next())
1074 {
1075 if (pPos->getByClient() == iFromClient)
1076 {
1077 if (++loadsAtClient >= C4NetResMaxLoadPerPeerPerFile)
1078 return true;
1079 }
1080 }
1081 // find chunk to retrieve
1082 int32_t iLoads[C4NetResMaxLoad]; int32_t i = 0;
1083 for (C4Network2ResLoad *pLoad = pLoads; pLoad; pLoad = pLoad->Next())
1084 iLoads[i++] = pLoad->getChunk();
1085 int32_t iRetrieveChunk = Chunks.GetChunkToRetrieve(Available, iLoadingCnt: i, pLoading: iLoads);
1086 // nothing? ignore
1087 if (iRetrieveChunk < 0 || static_cast<uint32_t>(iRetrieveChunk) >= Core.getChunkCnt())
1088 return true;
1089 // search message connection for client
1090 C4Network2IOConnection *pConn = pParent->getIOClass()->GetMsgConnection(iClientID: iFromClient);
1091 if (!pConn) return false;
1092 // send request
1093 if (!pConn->Send(rPkt: MkC4NetIOPacket(cStatus: PID_NetResReq, Pkt: C4PacketResRequest(Core.getID(), iRetrieveChunk))))
1094 {
1095 pConn->DelRef(); return false;
1096 }
1097 pConn->DelRef();
1098#ifdef C4NET2RES_DEBUG_LOG
1099 // log
1100 pParent->logger->trace("Res: requesting chunk {} of {}:{} ({}) from client {}",
1101 iRetrieveChunk, Core.getID(), Core.getFileName(), szFile, iFromClient);
1102#endif
1103 // create load class
1104 C4Network2ResLoad *pnLoad = new C4Network2ResLoad(iRetrieveChunk, iFromClient);
1105 // add to list
1106 pnLoad->pNext = pLoads;
1107 pLoads = pnLoad;
1108 iLoadCnt++;
1109 // ok
1110 return true;
1111}
1112
1113void C4Network2Res::EndLoad()
1114{
1115 // clear loading data
1116 ClearLoad();
1117 // set complete
1118 fLoading = false;
1119 // call handler
1120 assert(pParent);
1121 pParent->OnResComplete(pRes: this);
1122}
1123
1124void C4Network2Res::ClearLoad()
1125{
1126 // remove client chunks and loads
1127 fLoading = false;
1128 while (pCChunks) RemoveCChunks(pChunks: pCChunks);
1129 while (pLoads) RemoveLoad(pLoad: pLoads);
1130 iDiscoverStartTime = iLoadCnt = 0;
1131}
1132
1133void C4Network2Res::RemoveLoad(C4Network2ResLoad *pLoad)
1134{
1135 if (pLoad == pLoads)
1136 pLoads = pLoad->Next();
1137 else
1138 {
1139 // find previous entry
1140 C4Network2ResLoad *pPrev;
1141 for (pPrev = pLoads; pPrev && pPrev->Next() != pLoad; pPrev = pPrev->Next());
1142 // remove
1143 if (pPrev)
1144 pPrev->pNext = pLoad->Next();
1145 }
1146 // delete
1147 delete pLoad;
1148 iLoadCnt--;
1149}
1150
1151void C4Network2Res::RemoveCChunks(ClientChunks *pChunks)
1152{
1153 if (pChunks == pCChunks)
1154 pCChunks = pChunks->Next;
1155 else
1156 {
1157 // find previous entry
1158 ClientChunks *pPrev;
1159 for (pPrev = pCChunks; pPrev && pPrev->Next != pChunks; pPrev = pPrev->Next);
1160 // remove
1161 if (pPrev)
1162 pPrev->Next = pChunks->Next;
1163 }
1164 // delete
1165 delete pChunks;
1166}
1167
1168bool C4Network2Res::OptimizeStandalone(bool fSilent)
1169{
1170 CStdLock FileLock(&FileCSec);
1171 // for now: player files only
1172 if (Core.getType() == NRT_Player)
1173 {
1174 // log - this may take a few seconds
1175 if (!fSilent) Log(id: C4ResStrTableKey::IDS_PRC_NETPREPARING, args: GetFilename(path: szFile));
1176 // copy to temp file, if needed
1177 if (!fTempFile && SEqual(szStr1: szFile, szStr2: szStandalone))
1178 {
1179 char szNewStandalone[_MAX_PATH + 1];
1180 if (!pParent->FindTempResFileName(szFilename: szStandalone, pTarget: szNewStandalone))
1181 {
1182 if (!fSilent) pParent->logger->error(msg: "OptimizeStandalone: could not find free name for temporary file!"); return false;
1183 }
1184 if (!C4Group_CopyItem(szSource: szStandalone, szTarget: szNewStandalone))
1185 {
1186 if (!fSilent) pParent->logger->error(msg: "OptimizeStandalone: could not copy to temporary file!"); return false;
1187 } /* TODO: Test failure */
1188 SCopy(szSource: szNewStandalone, sTarget: szStandalone, iMaxL: sizeof(szStandalone) - 1);
1189 }
1190 // open as group
1191 C4Group Grp;
1192 if (!Grp.Open(szGroupName: szStandalone))
1193 {
1194 if (!fSilent) pParent->logger->error(msg: "OptimizeStandalone: could not open player file!"); return false;
1195 }
1196 // remove portrais
1197 Grp.Delete(C4CFN_Portraits, fRecursive: true);
1198 // remove bigicon, if the file size is too large
1199 size_t iBigIconSize = 0;
1200 if (Grp.FindEntry(C4CFN_BigIcon, sFileName: nullptr, iSize: &iBigIconSize))
1201 if (iBigIconSize > C4NetResMaxBigicon * 1024)
1202 Grp.Delete(C4CFN_BigIcon);
1203 Grp.Close();
1204 }
1205 return true;
1206}
1207
1208bool C4Network2Res::GetClientProgress(int32_t clientID, int32_t &presentChunkCnt, int32_t &chunkCnt)
1209{
1210 // Try to find chunks for client ID
1211 ClientChunks *chunks;
1212 for (chunks = pCChunks; chunks; chunks = chunks->Next)
1213 {
1214 if (chunks->ClientID == clientID) break;
1215 }
1216
1217 if (!chunks) return false; // Not found?
1218
1219 presentChunkCnt = chunks->Chunks.getPresentChunkCnt();
1220 chunkCnt = Chunks.getChunkCnt();
1221 return true;
1222}
1223
1224// *** C4Network2ResChunk
1225
1226C4Network2ResChunk::C4Network2ResChunk() {}
1227
1228C4Network2ResChunk::~C4Network2ResChunk() {}
1229
1230bool C4Network2ResChunk::Set(C4Network2Res *pRes, uint32_t inChunk)
1231{
1232 const auto &logger = pRes->pParent->GetLogger();
1233 const C4Network2ResCore &Core = pRes->getCore();
1234 iResID = pRes->getResID();
1235 iChunk = inChunk;
1236 // calculate offset and size
1237 int32_t iOffset = iChunk * Core.getChunkSize(),
1238 iSize = std::min<int32_t>(a: Core.getFileSize() - iOffset, b: C4NetResChunkSize);
1239 if (iSize < 0) { logger->error(fmt: "could not get chunk from offset {} from resource file {}: File size is only {}!", args&: iOffset, args: pRes->getFile(), args: Core.getFileSize()); return false; }
1240 // open file
1241 int32_t f = pRes->OpenFileRead();
1242 if (f == -1) { logger->error(fmt: "could not open resource file {}!", args: pRes->getFile()); return false; }
1243 // seek
1244 if (iOffset)
1245 if (lseek(fd: f, offset: iOffset, SEEK_SET) != iOffset)
1246 {
1247 close(fd: f); logger->error(fmt: "could not read resource file {}!", args: pRes->getFile()); return false;
1248 }
1249 // read chunk of data
1250 char *pBuf = static_cast<char *>(malloc(size: sizeof(char) * iSize));
1251 if (read(fd: f, buf: pBuf, nbytes: iSize) != iSize)
1252 {
1253 free(ptr: pBuf); close(fd: f); logger->error(fmt: "could not read resource file {}!", args: pRes->getFile()); return false;
1254 }
1255 // set
1256 Data.Take(pnData: pBuf, inSize: iSize);
1257 // close
1258 close(fd: f);
1259 // ok
1260 return true;
1261}
1262
1263bool C4Network2ResChunk::AddTo(C4Network2Res *pRes, C4Network2IO *pIO) const
1264{
1265 assert(pRes); assert(pIO);
1266#ifdef C4NET2RES_DEBUG_LOG
1267 const auto &logger = pRes->pParent->GetLogger();
1268#endif
1269 const C4Network2ResCore &Core = pRes->getCore();
1270 // check
1271 if (iResID != pRes->getResID())
1272 {
1273#ifdef C4NET2RES_DEBUG_LOG
1274 logger->trace("C4Network2ResChunk({})::AddTo({} [{}]): Ressource ID mismatch!", iResID, Core.getFileName(), pRes->getResID());
1275#endif
1276 return false;
1277 }
1278 // calculate offset and size
1279 int32_t iOffset = iChunk * Core.getChunkSize();
1280 if (iOffset + Data.getSize() > Core.getFileSize())
1281 {
1282#ifdef C4NET2RES_DEBUG_LOG
1283 logger->trace("C4Network2ResChunk({})::AddTo({} [{}]): Adding {} bytes at offset {} exceeds expected file size of {}!", iResID, Core.getFileName(), pRes->getResID(), Data.getSize(), iOffset, Core.getFileSize());
1284#endif
1285 return false;
1286 }
1287 // open file
1288 int32_t f = pRes->OpenFileWrite();
1289 if (f == -1)
1290 {
1291#ifdef C4NET2RES_DEBUG_LOG
1292 logger->trace("C4Network2ResChunk({})::AddTo({} [{}]): Open write file error: {}!", iResID, Core.getFileName(), pRes->getResID(), strerror(errno));
1293#endif
1294 return false;
1295 }
1296 // seek
1297 if (iOffset)
1298 if (lseek(fd: f, offset: iOffset, SEEK_SET) != iOffset)
1299 {
1300#ifdef C4NET2RES_DEBUG_LOG
1301 logger->trace("C4Network2ResChunk({})::AddTo({} [{}]): lseek file error: {}!", iResID, Core.getFileName(), pRes->getResID(), strerror(errno));
1302#endif
1303 close(fd: f);
1304 return false;
1305 }
1306 // write
1307 if (write(fd: f, buf: Data.getData(), n: Data.getSize()) != int32_t(Data.getSize()))
1308 {
1309#ifdef C4NET2RES_DEBUG_LOG
1310 logger->trace("C4Network2ResChunk({})::AddTo({} [{}]): write error: {}!", iResID, Core.getFileName(), pRes->getResID(), strerror(errno));
1311#endif
1312 close(fd: f);
1313 return false;
1314 }
1315 // ok, add chunks
1316 close(fd: f);
1317 pRes->Chunks.AddChunk(iChunk);
1318 return true;
1319}
1320
1321void C4Network2ResChunk::CompileFunc(StdCompiler *pComp)
1322{
1323 // pack header
1324 pComp->Value(rStruct: mkNamingAdapt(rValue&: iResID, szName: "ResID", rDefault: -1));
1325 pComp->Value(rStruct: mkNamingAdapt(rValue&: iChunk, szName: "Chunk", rDefault: ~0U));
1326 // Data
1327 pComp->Value(rStruct: mkNamingAdapt(rValue&: Data, szName: "Data"));
1328}
1329
1330// *** C4Network2ResList
1331
1332C4Network2ResList::C4Network2ResList()
1333 : iClientID(-1),
1334 iNextResID((-1) << 16),
1335 pFirst(nullptr),
1336 ResListCSec(this),
1337 iLastDiscover(0), iLastStatus(0),
1338 pIO(nullptr) {}
1339
1340C4Network2ResList::~C4Network2ResList()
1341{
1342 Clear();
1343}
1344
1345bool C4Network2ResList::Init(std::shared_ptr<spdlog::logger> logger, int32_t inClientID, C4Network2IO *pIOClass) // by main thread
1346{
1347 // clear old list
1348 Clear();
1349 this->logger = std::move(logger);
1350 // safe IO class
1351 pIO = pIOClass;
1352 // set client id
1353 iNextResID = iClientID = 0;
1354 SetLocalID(inClientID);
1355 // create network path
1356 if (!CreateNetworkFolder()) return false;
1357 // ok
1358 return true;
1359}
1360
1361void C4Network2ResList::SetLocalID(int32_t inClientID)
1362{
1363 CStdLock ResIDLock(&ResIDCSec);
1364 int32_t iOldClientID = iClientID;
1365 int32_t iIDDiff = (inClientID - iClientID) << 16;
1366 // set new counter
1367 iClientID = inClientID;
1368 iNextResID += iIDDiff;
1369 // change ressource ids
1370 CStdLock ResListLock(&ResListCSec);
1371 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1372 if (pRes->getResClient() == iOldClientID)
1373 pRes->ChangeID(inID: pRes->getResID() + iIDDiff);
1374}
1375
1376int32_t C4Network2ResList::nextResID() // by main thread
1377{
1378 CStdLock ResIDLock(&ResIDCSec);
1379 assert(iNextResID >= (iClientID << 16));
1380 if (iNextResID >= ((iClientID + 1) << 16) - 1)
1381 iNextResID = std::max<int32_t>(a: 0, b: iClientID) << 16;
1382 // find free
1383 while (getRes(iResID: iNextResID))
1384 iNextResID++;
1385 return iNextResID++;
1386}
1387
1388C4Network2Res *C4Network2ResList::getRes(int32_t iResID)
1389{
1390 CStdShareLock ResListLock(&ResListCSec);
1391 for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
1392 if (pCur->getResID() == iResID)
1393 return pCur;
1394 return nullptr;
1395}
1396
1397C4Network2Res *C4Network2ResList::getRes(const char *szFile, bool fLocalOnly)
1398{
1399 CStdShareLock ResListLock(&ResListCSec);
1400 for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
1401 if (!pCur->isAnonymous())
1402 if (SEqual(szStr1: pCur->getFile(), szStr2: szFile))
1403 if (!fLocalOnly || pCur->getResClient() == iClientID)
1404 return pCur;
1405 return nullptr;
1406}
1407
1408C4Network2Res::Ref C4Network2ResList::getRefRes(int32_t iResID)
1409{
1410 CStdShareLock ResListLock(&ResListCSec);
1411 return getRes(iResID);
1412}
1413
1414C4Network2Res::Ref C4Network2ResList::getRefRes(const char *szFile, bool fLocalOnly)
1415{
1416 CStdShareLock ResListLock(&ResListCSec);
1417 return getRes(szFile, fLocalOnly);
1418}
1419
1420C4Network2Res::Ref C4Network2ResList::getRefNextRes(int32_t iResID)
1421{
1422 CStdShareLock ResListLock(&ResListCSec);
1423 C4Network2Res *pRes = nullptr;
1424 for (C4Network2Res *pCur = pFirst; pCur; pCur = pCur->pNext)
1425 if (!pCur->isRemoved() && pCur->getResID() >= iResID)
1426 if (!pRes || pRes->getResID() > pCur->getResID())
1427 pRes = pCur;
1428 return pRes;
1429}
1430
1431void C4Network2ResList::Add(C4Network2Res *pRes)
1432{
1433 // get locks
1434 CStdShareLock ResListLock(&ResListCSec);
1435 CStdLock ResListAddLock(&ResListAddCSec);
1436 // reference
1437 pRes->AddRef();
1438 // add
1439 pRes->pNext = pFirst;
1440 pFirst = pRes;
1441}
1442
1443C4Network2Res::Ref C4Network2ResList::AddByFile(const char *strFilePath, bool fTemp, C4Network2ResType eType, int32_t iResID, const char *szResName, bool fAllowUnloadable)
1444{
1445 // already in list?
1446 if (C4Network2Res::Ref pRes = getRefRes(szFile: strFilePath); pRes)
1447 {
1448 return pRes;
1449 }
1450
1451 // get ressource ID
1452 if (iResID < 0) iResID = nextResID();
1453 if (iResID < 0) { logger->error(msg: "AddByFile: no more ressource IDs available!"); return nullptr; }
1454 // create new
1455 auto res = std::make_unique<C4Network2Res>(args: this);
1456 // initialize
1457 if (!res->SetByFile(strFilePath, fTemp, eType, iResID, szResName)) { return nullptr; }
1458 // create standalone for non-system files
1459 // system files shouldn't create a standalone; they should never be marked loadable!
1460 if (eType != NRT_System)
1461 if (!res->GetStandalone(pTo: nullptr, iMaxL: 0, fSetOfficial: true, fAllowUnloadable))
1462 if (!fAllowUnloadable)
1463 {
1464 return nullptr;
1465 }
1466
1467 // add to list
1468 const auto resPtr = res.release();
1469 Add(pRes: resPtr);
1470 return resPtr;
1471}
1472
1473C4Network2Res::Ref C4Network2ResList::AddByCore(const C4Network2ResCore &Core, bool fLoad) // by main thread
1474{
1475 // already in list?
1476 C4Network2Res::Ref pRes = getRefRes(iResID: Core.getID());
1477 if (pRes) return pRes;
1478#ifdef C4NET2RES_LOAD_ALL
1479 // load without check (if possible)
1480 if (Core.isLoadable()) return AddLoad(Core);
1481#endif
1482 // create new
1483 pRes = new C4Network2Res(this);
1484 // try set by core
1485 if (!pRes->SetByCore(nCore: Core, fSilent: true))
1486 {
1487 pRes.Clear();
1488 // try load (if specified)
1489 return fLoad ? AddLoad(Core) : nullptr;
1490 }
1491 // log
1492 logger->info(fmt: "Found identical {}. Not loading.", args: pRes->getCore().getFileName());
1493 // add to list
1494 Add(pRes);
1495 // ok
1496 return pRes;
1497}
1498
1499C4Network2Res::Ref C4Network2ResList::AddLoad(const C4Network2ResCore &Core) // by main thread
1500{
1501 // marked unloadable by creator?
1502 if (!Core.isLoadable())
1503 {
1504 // show error msg
1505 logger->error(fmt: "Cannot load {} (marked unloadable)", args: Core.getFileName());
1506 return nullptr;
1507 }
1508 // create new
1509 C4Network2Res::Ref pRes = new C4Network2Res(this);
1510 // initialize
1511 pRes->SetLoad(Core);
1512 // log
1513 logger->info(fmt: "loading {}...", args: Core.getFileName());
1514 // add to list
1515 Add(pRes);
1516 return pRes;
1517}
1518
1519void C4Network2ResList::RemoveAtClient(int32_t iClientID) // by main thread
1520{
1521 CStdShareLock ResListLock(&ResListCSec);
1522 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1523 if (pRes->getResClient() == iClientID)
1524 pRes->Remove();
1525}
1526
1527void C4Network2ResList::Clear()
1528{
1529 CStdShareLock ResListLock(&ResListCSec);
1530 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1531 {
1532 pRes->Remove();
1533 pRes->iLastReqTime = 0;
1534 }
1535 iClientID = C4ClientIDUnknown;
1536 iLastDiscover = iLastStatus = 0;
1537 logger.reset();
1538}
1539
1540void C4Network2ResList::OnClientConnect(C4Network2IOConnection *pConn) // by main thread
1541{
1542 // discover ressources
1543 SendDiscover(pTo: pConn);
1544}
1545
1546void C4Network2ResList::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2IOConnection *pConn)
1547{
1548 // security
1549 if (!pConn) return;
1550
1551#define GETPKT(type, name) \
1552 assert(pPacket); \
1553 const type &name = static_cast<const type &>(*pPacket);
1554
1555 switch (cStatus)
1556 {
1557 case PID_NetResDis: // ressource discover
1558 {
1559 if (!pConn->isOpen()) break;
1560 GETPKT(C4PacketResDiscover, Pkt);
1561 // search matching ressources
1562 CStdShareLock ResListLock(&ResListCSec);
1563 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1564 if (Pkt.isIDPresent(iID: pRes->getResID()))
1565 // must be binary compatible
1566 if (pRes->IsBinaryCompatible())
1567 pRes->OnDiscover(pBy: pConn);
1568 }
1569 break;
1570
1571 case PID_NetResStat: // ressource status
1572 {
1573 if (!pConn->isOpen()) break;
1574 GETPKT(C4PacketResStatus, Pkt);
1575 // matching ressource?
1576 CStdShareLock ResListLock(&ResListCSec);
1577 C4Network2Res *pRes = getRes(iResID: Pkt.getResID());
1578 // present / being loaded? call handler
1579 if (pRes)
1580 pRes->OnStatus(rChunkData: Pkt.getChunks(), pBy: pConn);
1581 }
1582 break;
1583
1584 case PID_NetResDerive: // ressource derive
1585 {
1586 GETPKT(C4Network2ResCore, Core);
1587 if (Core.getDerID() < 0) break;
1588 // Check if there is a anonymous derived ressource with matching parent.
1589 CStdShareLock ResListLock(&ResListCSec);
1590 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1591 if (pRes->isAnonymous() && pRes->getCore().getDerID() == Core.getDerID())
1592 pRes->FinishDerive(nCore: Core);
1593 }
1594 break;
1595
1596 case PID_NetResReq: // ressource request
1597 {
1598 GETPKT(C4PacketResRequest, Pkt);
1599 // find ressource
1600 CStdShareLock ResListLock(&ResListCSec);
1601 C4Network2Res *pRes = getRes(iResID: Pkt.getReqID());
1602 // send requested chunk
1603 if (pRes && pRes->IsBinaryCompatible()) pRes->SendChunk(iChunk: Pkt.getReqChunk(), iToClient: pConn->getClientID());
1604 }
1605 break;
1606
1607 case PID_NetResData: // a chunk of data is coming in
1608 {
1609 GETPKT(C4Network2ResChunk, Chunk);
1610 // find ressource
1611 CStdShareLock ResListLock(&ResListCSec);
1612 C4Network2Res *pRes = getRes(iResID: Chunk.getResID());
1613 // send requested chunk
1614 if (pRes) pRes->OnChunk(rChunk: Chunk);
1615 }
1616 break;
1617 }
1618#undef GETPKT
1619}
1620
1621void C4Network2ResList::OnTimer()
1622{
1623 CStdShareLock ResListLock(&ResListCSec);
1624 C4Network2Res *pRes;
1625 // do loads, check timeouts
1626 for (pRes = pFirst; pRes; pRes = pRes->pNext)
1627 if (pRes->isLoading() && !pRes->isRemoved())
1628 if (!pRes->DoLoad())
1629 pRes->Remove();
1630 // discovery time?
1631 if (!iLastDiscover || difftime(time1: time(timer: nullptr), time0: iLastDiscover) >= C4NetResDiscoverInterval)
1632 {
1633 // needed?
1634 bool fSendDiscover = false;
1635 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1636 if (!pRes->isRemoved())
1637 fSendDiscover |= pRes->NeedsDiscover();
1638 // send
1639 if (fSendDiscover)
1640 SendDiscover();
1641 }
1642 // status update?
1643 if (!iLastStatus || difftime(time1: time(timer: nullptr), time0: iLastStatus) >= C4NetResStatusInterval)
1644 {
1645 // any?
1646 bool fStatusUpdates = false;
1647 for (pRes = pFirst; pRes; pRes = pRes->pNext)
1648 if (pRes->isDirty() && !pRes->isRemoved())
1649 fStatusUpdates |= pRes->SendStatus();
1650 // set time accordingly
1651 iLastStatus = fStatusUpdates ? time(timer: nullptr) : 0;
1652 }
1653}
1654
1655void C4Network2ResList::OnShareFree(CStdCSecEx *pCSec)
1656{
1657 if (pCSec == &ResListCSec)
1658 {
1659 // remove entries
1660 for (C4Network2Res *pRes = pFirst, *pNext, *pPrev = nullptr; pRes; pRes = pNext)
1661 {
1662 pNext = pRes->pNext;
1663 if (pRes->isRemoved() && (!pRes->getLastReqTime() || difftime(time1: time(timer: nullptr), time0: pRes->getLastReqTime()) > C4NetResDeleteTime))
1664 {
1665 // unlink
1666 (pPrev ? pPrev->pNext : pFirst) = pNext;
1667 // remove
1668 pRes->pNext = nullptr;
1669 pRes->DelRef();
1670 }
1671 else
1672 pPrev = pRes;
1673 }
1674 }
1675}
1676
1677bool C4Network2ResList::SendDiscover(C4Network2IOConnection *pTo) // by both
1678{
1679 // make packet
1680 C4PacketResDiscover Pkt;
1681 // add special retrieves
1682 CStdShareLock ResListLock(&ResListCSec);
1683 for (C4Network2Res *pRes = pFirst; pRes; pRes = pRes->pNext)
1684 if (!pRes->isRemoved())
1685 Pkt.AddDisID(iID: pRes->getResID());
1686 ResListLock.Clear();
1687 // empty?
1688 if (!Pkt.getDisIDCnt()) return false;
1689 // broadcast?
1690 if (!pTo)
1691 {
1692 // save time
1693 iLastDiscover = time(timer: nullptr);
1694 // send
1695 return pIO->BroadcastMsg(rPkt: MkC4NetIOPacket(cStatus: PID_NetResDis, Pkt));
1696 }
1697 else
1698 return pTo->Send(rPkt: MkC4NetIOPacket(cStatus: PID_NetResDis, Pkt));
1699}
1700
1701void C4Network2ResList::OnResComplete(C4Network2Res *pRes)
1702{
1703 // log
1704 logger->info(fmt: "{} received.", args: pRes->getCore().getFileName());
1705 // call handler (ctrl might wait for this ressource)
1706 Game.Control.Network.OnResComplete(pRes);
1707}
1708
1709bool C4Network2ResList::CreateNetworkFolder()
1710{
1711 // get network path without trailing backslash
1712 char szNetworkPath[_MAX_PATH + 1];
1713 SCopy(szSource: Config.Network.WorkPath, sTarget: szNetworkPath, _MAX_PATH);
1714 TruncateBackslash(szFilename: szNetworkPath);
1715 // but make sure that the configured path has one
1716 AppendBackslash(szFileName: Config.Network.WorkPath);
1717 // does not exist?
1718 if (access(name: szNetworkPath, type: 00))
1719 {
1720 if (!MakeDirectory(pathname: szNetworkPath, nullptr))
1721 {
1722 LogFatalNTr(message: "could not create network path!"); return false;
1723 }
1724 return true;
1725 }
1726 // stat
1727 struct stat s;
1728 if (stat(file: szNetworkPath, buf: &s))
1729 {
1730 LogFatalNTr(message: "could not stat network path!"); return false;
1731 }
1732 // not a subdir?
1733 if (!(s.st_mode & S_IFDIR))
1734 {
1735 LogFatalNTr(message: "could not create network path: blocked by a file!"); return false;
1736 }
1737 // ok
1738 return true;
1739}
1740
1741bool C4Network2ResList::FindTempResFileName(const char *szFilename, char *pTarget)
1742{
1743 static constexpr auto newFileCreated = [](const char *const filename)
1744 {
1745 FILE *const file{fopen(filename: filename, modes: "wxb")};
1746 if (file)
1747 {
1748 fclose(stream: file);
1749 }
1750
1751 return file != nullptr;
1752 };
1753
1754 char safeFilename[_MAX_PATH];
1755 char *safePos = safeFilename;
1756 while (*szFilename)
1757 {
1758 if ((*szFilename >= 'a' && *szFilename <= 'z') ||
1759 (*szFilename >= 'A' && *szFilename <= 'Z') ||
1760 (*szFilename >= '0' && *szFilename <= '9') ||
1761 (*szFilename == '.') || (*szFilename == '/'))
1762 *safePos = *szFilename;
1763 else
1764 *safePos = '_';
1765
1766 ++safePos;
1767 ++szFilename;
1768 }
1769 *safePos = 0;
1770 szFilename = safeFilename;
1771
1772 // create temporary file
1773 SCopy(szSource: Config.AtNetworkPath(szFilename: GetFilename(path: szFilename)), sTarget: pTarget, _MAX_PATH);
1774 // file name is free?
1775 if (newFileCreated(pTarget)) return true;
1776 // find another file name
1777 char szFileMask[_MAX_PATH + 1];
1778 SCopy(szSource: pTarget, sTarget: szFileMask, iMaxL: GetExtension(fname: pTarget) - 1 - pTarget);
1779 char *const end{szFileMask + std::strlen(s: szFileMask) + 1};
1780 end[-1] = '_';
1781
1782 for (int32_t i = 2; i < 1000; i++)
1783 {
1784 char *const extPtr{std::to_chars(first: end, last: szFileMask + std::size(szFileMask) - 1, value: i).ptr};
1785 SCopy(szSource: GetExtension(fname: pTarget) - 1, sTarget: extPtr, _MAX_PATH - (extPtr - szFileMask));
1786 SCopy(szSource: szFileMask, sTarget: pTarget, _MAX_PATH);
1787 // doesn't exist?
1788 if (newFileCreated(pTarget))
1789 return true;
1790 }
1791 // not found
1792 return false;
1793}
1794
1795int32_t C4Network2ResList::GetClientProgress(int32_t clientID)
1796{
1797 int32_t sumPresentChunkCnt = 0, sumChunkCnt = 0;
1798 CStdLock ResListLock(&ResListCSec);
1799 for (C4Network2Res *res = pFirst; res; res = res->pNext)
1800 {
1801 int32_t presentChunkCnt, chunkCnt;
1802 if (res->isRemoved() || !res->GetClientProgress(clientID, presentChunkCnt, chunkCnt)) continue;
1803 sumPresentChunkCnt += presentChunkCnt;
1804 sumChunkCnt += chunkCnt;
1805 }
1806 return sumChunkCnt == 0 ? 100 : sumPresentChunkCnt * 100 / sumChunkCnt;
1807}
1808