1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// a set of group files
19// manages system file overwriting by scearios or folders
20
21#include <C4GroupSet.h>
22
23#ifndef USE_CONSOLE
24#include <C4Game.h>
25#else
26#include "C4Components.h"
27#include "C4Group.h"
28#endif
29
30#include <C4Log.h>
31#include "C4ResStrTable.h"
32
33C4GroupSetNode::C4GroupSetNode(C4GroupSet &rParent, C4GroupSetNode *pPrev, C4Group &rGroup, bool fGrpOwned, int32_t id)
34{
35 // set parent
36 pParent = &rParent;
37 // link into list
38 this->pPrev = pPrev;
39 if (pPrev) { pNext = pPrev->pNext; pPrev->pNext = this; }
40 else { pNext = pParent->pFirst; pParent->pFirst = this; }
41 if (pNext) pNext->pPrev = this; else pParent->pLast = this;
42 // set group
43 pGroup = &rGroup;
44 this->fGrpOwned = fGrpOwned;
45 // set id
46 this->id = id;
47}
48
49C4GroupSetNode::~C4GroupSetNode()
50{
51 // remove group
52 if (fGrpOwned) delete pGroup;
53 // unlink from list
54 (pPrev ? pPrev->pNext : pParent->pFirst) = pNext;
55 (pNext ? pNext->pPrev : pParent->pLast) = pPrev;
56}
57
58void C4GroupSet::Clear()
59{
60 // clear nodes
61 while (pFirst) delete pFirst;
62 pFirst = nullptr;
63}
64
65void C4GroupSet::Default()
66{
67 // zero fields
68 pFirst = pLast = nullptr;
69 // do not reset index here, because group set IDs are meant to be unique
70 // for each instance of the engine
71 // see also C4GraphicsResource::RegisterGlobalGraphics
72}
73
74C4GroupSet::C4GroupSet()
75{
76 // zero fields
77 Default();
78 iIndex = 0;
79}
80
81C4GroupSet::~C4GroupSet()
82{
83 // clear nodes
84 Clear();
85}
86
87bool C4GroupSet::RegisterGroup(C4Group &rGroup, bool fOwnGrp, int32_t Priority, int32_t Contents, bool fCheckContent)
88{
89 // get node to sort in
90 // begin at back end and search for higher priority
91 C4GroupSetNode *pNode;
92 for (pNode = pLast; pNode; pNode = pNode->pPrev)
93 if (pNode->Priority > Priority) break;
94 // create new node
95 C4GroupSetNode *pNewNode = new C4GroupSetNode(*this, pNode, rGroup, fOwnGrp, ++iIndex);
96 // check content
97 if (fCheckContent) Contents = CheckGroupContents(rGroup, Contents);
98 // set priority and contents mask
99 pNewNode->Priority = Priority;
100 pNewNode->Contents = Contents;
101
102#ifndef USE_CONSOLE
103 // always add fonts directly
104 if (Contents & C4GSCnt_FontDefs)
105 Game.FontLoader.LoadDefs(hGroup&: rGroup, rCfg&: Config);
106#endif
107
108 // success
109 return true;
110}
111
112int32_t C4GroupSet::CheckGroupContents(C4Group &rGroup, int32_t Contents)
113{
114 // update mask
115 if (Contents & C4GSCnt_Graphics) if (!rGroup.FindEntry(C4CFN_Graphics)) Contents = Contents & ~C4GSCnt_Graphics;
116 if (Contents & C4GSCnt_Loaders)
117 {
118 if (!rGroup.FindEntry(szWildCard: "Loader*.bmp")
119 && !rGroup.FindEntry(szWildCard: "Loader*.png")
120 && !rGroup.FindEntry(szWildCard: "Loader*.jpg")
121 && !rGroup.FindEntry(szWildCard: "Loader*.jpeg"))
122 {
123 Contents = Contents & ~C4GSCnt_Loaders;
124 }
125 }
126 if (Contents & C4GSCnt_Material) if (!rGroup.FindEntry(C4CFN_Material)) Contents = Contents & ~C4GSCnt_Material;
127 if (Contents & C4GSCnt_Music) if (!rGroup.FindEntry(C4CFN_Music)) Contents = Contents & ~C4GSCnt_Music;
128 if (Contents & C4GSCnt_Definitions) if (!rGroup.FindEntry(C4CFN_DefFiles)) Contents = Contents & ~C4GSCnt_Definitions;
129 if (Contents & C4GSCnt_FontDefs) if (!rGroup.FindEntry(C4CFN_FontFiles)) if (!rGroup.FindEntry(C4CFN_FontDefs)) Contents = Contents & ~C4GSCnt_FontDefs;
130 // return it
131 return Contents;
132}
133
134bool C4GroupSet::RegisterGroups(C4GroupSet &rCopy, int32_t Contents, const char *szFilename, int32_t iMaxSkipID)
135{
136 // get all groups of rCopy
137 int32_t Contents2;
138 for (C4GroupSetNode *pNode = rCopy.pFirst; pNode; pNode = pNode->pNext)
139 if (Contents2 = pNode->Contents & Contents)
140 if (pNode->id > iMaxSkipID)
141 if (!szFilename)
142 // add group but don't check the content again!
143 RegisterGroup(rGroup&: *pNode->pGroup, fOwnGrp: false, Priority: pNode->Priority, Contents: Contents2, fCheckContent: false);
144 else
145 {
146 // if a filename is given, open the child group
147 C4Group *pGroup = new C4Group();
148 if (!pGroup->OpenAsChild(pMother: pNode->pGroup, szEntryName: szFilename))
149 {
150 delete pGroup; continue;
151 }
152 // add the child group to the local list; contents equal Contents2
153 // but this flag is not likely to be used
154 if (!RegisterGroup(rGroup&: *pGroup, fOwnGrp: true, Priority: pNode->Priority, Contents: Contents2, fCheckContent: false))
155 delete pGroup;
156 }
157 // done, success
158 return true;
159}
160
161C4Group *C4GroupSet::FindGroup(int32_t Contents, C4Group *pAfter, bool fSamePrio)
162{
163 // get priority
164 int32_t iPriority = -1;
165 // find group by matching content mask
166 for (C4GroupSetNode *pNode = pFirst; pNode; pNode = pNode->pNext)
167 {
168 // check contents
169 if (!pAfter && (pNode->Contents & Contents))
170 // check priority
171 if (iPriority == -1 || iPriority == pNode->Priority)
172 // success, found an entry
173 return pNode->pGroup;
174 // find next clear flag
175 if (pNode->pGroup == pAfter) { pAfter = nullptr; if (fSamePrio) iPriority = pNode->Priority; }
176 }
177 // nothing found
178 return nullptr;
179}
180
181C4Group *C4GroupSet::FindEntry(const char *szWildcard, int32_t *pPriority, int32_t *pID)
182{
183 // find group that has this entry
184 for (C4GroupSetNode *pNode = pFirst; pNode; pNode = pNode->pNext)
185 if (pNode->pGroup->FindEntry(szWildCard: szWildcard))
186 {
187 // assign priority and ID, if ptrs is given
188 if (pPriority) *pPriority = pNode->Priority;
189 if (pID) *pID = pNode->id;
190 // return found group
191 return pNode->pGroup;
192 }
193 // nothing found
194 return nullptr;
195}
196
197bool C4GroupSet::LoadEntryString(const char *szEntryName, StdStrBuf &rBuf)
198{
199 // Load the entry from the first group that has it
200 C4Group *pGroup;
201 if (pGroup = FindEntry(szWildcard: szEntryName))
202 return pGroup->LoadEntryString(szEntryName, Buf&: rBuf);
203 // Didn't find it
204 return false;
205}
206
207bool C4GroupSet::CloseFolders()
208{
209 // close everything that has folder-priority
210 for (C4GroupSetNode *pNode = pFirst, *pNext; pNode; pNode = pNext)
211 {
212 // get next, as pNode might be destroyed
213 pNext = pNode->pNext;
214 // check if priority matches
215 if (Inside<int32_t>(ival: pNode->Priority, C4GSPrio_Folder, C4GSPrio_Folder2) || pNode->Priority == C4GSPrio_Scenario)
216 // clear it!
217 delete pNode;
218 }
219 // done, success
220 return true;
221}
222
223C4Group *C4GroupSet::GetGroup(int32_t iIndex)
224{
225 // Invalid index
226 if (iIndex < 0)
227 return nullptr;
228 // Find indicated group
229 for (C4GroupSetNode *pNode = pFirst; pNode; pNode = pNode->pNext)
230 if (iIndex == 0)
231 return pNode->pGroup;
232 else
233 iIndex--;
234 // Indicated group not found
235 return nullptr;
236}
237
238C4Group *C4GroupSet::RegisterParentFolders(const char *szScenFilename)
239{
240 // the scenario filename may be a scenario or directly a group folder
241 C4Group *pParentGroup = nullptr; bool fParentC4F;
242 char szParentfolder[_MAX_PATH + 1];
243 if (SEqualNoCase(szStr1: GetExtension(fname: szScenFilename), szStr2: "c4f"))
244 {
245 fParentC4F = true;
246 SCopy(szSource: szScenFilename, sTarget: szParentfolder, _MAX_PATH);
247 }
248 else
249 {
250 GetParentPath(szFilename: szScenFilename, szBuffer: szParentfolder);
251 fParentC4F = SEqualNoCase(szStr1: GetExtension(fname: szParentfolder), szStr2: "c4f");
252 }
253 if (fParentC4F)
254 {
255 // replace all (back)slashes with zero-fields
256 const auto iOriginalLen = SLen(sptr: szParentfolder);
257 for (size_t i = 0; i < iOriginalLen; ++i) if (szParentfolder[i] == DirectorySeparator || szParentfolder[i] == '/') szParentfolder[i] = 0;
258 // trace back until the file extension is no more .c4f
259 auto iPos = iOriginalLen - 1;
260 while (iPos)
261 {
262 // ignore additional zero fields (double-backslashes)
263 while (iPos && !szParentfolder[iPos]) --iPos;
264 // break if end has been reached
265 if (!iPos) break;
266 // trace back until next zero field
267 while (iPos && szParentfolder[iPos]) --iPos;
268 // check extension of this folder
269 if (!SEqualNoCase(szStr1: GetExtension(fname: szParentfolder + iPos + 1), szStr2: "c4f", iLen: 3)) break;
270 // continue
271 }
272 // trace backwards, putting the (back)slashes in place again
273 if (iPos)
274 {
275 szParentfolder[iPos + 1 + SLen(sptr: szParentfolder + iPos + 1)] = szParentfolder[iPos] = DirectorySeparator;
276 while (iPos--) if (!szParentfolder[iPos]) szParentfolder[iPos] = DirectorySeparator;
277 ++iPos;
278 }
279 // trace forward again, opening all folders (iPos is 0 again)
280 int32_t iGroupIndex = 0;
281 while (iPos < iOriginalLen)
282 {
283 // ignore additional zero-fields
284 while (iPos < iOriginalLen && !szParentfolder[iPos]) ++iPos;
285 // break if end has been reached
286 if (iPos >= iOriginalLen) break;
287 // open this folder
288 C4Group *pGroup = new C4Group();
289 if (pParentGroup)
290 {
291 if (!pGroup->OpenAsChild(pMother: pParentGroup, szEntryName: szParentfolder + iPos))
292 {
293 LogFatalNTr(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND), args: szParentfolder + iPos);
294 delete pGroup; return nullptr;
295 }
296 }
297 else if (!pGroup->Open(szGroupName: szParentfolder + iPos))
298 {
299 LogFatalNTr(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND), args: szParentfolder + iPos);
300 delete pGroup; return nullptr;
301 }
302 // set this group as new parent
303 pParentGroup = pGroup;
304 // add to group set, if this is a true scenario folder
305 int32_t iContentsMask;
306 if (WildcardMatch(C4CFN_FolderFiles, szFName2: pParentGroup->GetName()))
307 iContentsMask = C4GSCnt_Folder;
308 else
309 iContentsMask = C4GSCnt_Directory;
310 if (!RegisterGroup(rGroup&: *pParentGroup, fOwnGrp: true, C4GSPrio_Folder + iGroupIndex++, Contents: iContentsMask))
311 {
312 delete pParentGroup; LogFatalNTr(message: "RegGrp: internal error"); return nullptr;
313 }
314 // advance by file name length
315 iPos += SLen(sptr: szParentfolder + iPos);
316 }
317 }
318 return pParentGroup;
319}
320