1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2022, 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/* Holds crew member information */
18
19#include <C4Include.h>
20#include <C4ObjectInfo.h>
21
22#include <C4Wrappers.h>
23#include <C4Random.h>
24#include <C4Components.h>
25#include <C4Game.h>
26#include <C4Config.h>
27#include <C4Application.h>
28#include <C4RankSystem.h>
29#include <C4Log.h>
30#include <C4Player.h>
31
32C4ObjectInfo::C4ObjectInfo()
33{
34 Default();
35}
36
37C4ObjectInfo::~C4ObjectInfo()
38{
39 Clear();
40}
41
42void C4ObjectInfo::Default()
43{
44 WasInAction = false;
45 InAction = false;
46 InActionTime = 0;
47 HasDied = false;
48 ControlCount = 0;
49 Filename[0] = 0;
50 Next = nullptr;
51 pDef = nullptr;
52 Portrait.Default();
53 pNewPortrait = nullptr;
54 pCustomPortrait = nullptr;
55}
56
57bool C4ObjectInfo::Load(C4Group &hMother, const char *szEntryname, bool fLoadPortrait)
58{
59 // New version
60 if (SEqualNoCase(szStr1: GetExtension(fname: szEntryname), szStr2: "c4i"))
61 {
62 C4Group hChild;
63 if (hChild.OpenAsChild(pMother: &hMother, szEntryName: szEntryname))
64 {
65 if (!C4ObjectInfo::Load(hGroup&: hChild, fLoadPortrait))
66 {
67 hChild.Close(); return false;
68 }
69 // resolve definition, if possible
70 pDef = C4Id2Def(id);
71 hChild.Close();
72 return true;
73 }
74 }
75
76 return false;
77}
78
79bool C4ObjectInfo::Load(C4Group &hGroup, bool fLoadPortrait)
80{
81 // Store group file name
82 SCopy(szSource: GetFilename(path: hGroup.GetName()), sTarget: Filename, _MAX_FNAME);
83 // Load core
84 if (!C4ObjectInfoCore::Load(hGroup)) return false;
85 // Load portrait - always try linking, even if fLoadPortrait is false (doesn't cost mem anyway)
86 // evaluate portrait string in info
87 bool fPortraitFileChecked = false;
88 if (*PortraitFile)
89 {
90 // custom portrait?
91 if (SEqual(szStr1: PortraitFile, C4Portrait_Custom))
92 {
93 // try to load it
94 delete pCustomPortrait;
95 pCustomPortrait = new C4Portrait();
96 if (pCustomPortrait->Load(rGrp&: hGroup, C4CFN_Portrait_Old, C4CFN_Portrait, C4CFN_PortraitOverlay))
97 {
98 // link portrait to custom portrait
99 Portrait.Link(pGfxPortrait: pCustomPortrait->GetGfx());
100 }
101 else
102 {
103 // load failure: reset portrait info
104 *PortraitFile = 0;
105 delete pCustomPortrait; pCustomPortrait = nullptr;
106 // do not try to load a custom portrait again
107 fPortraitFileChecked = true;
108 }
109 }
110 else if (SEqual(szStr1: PortraitFile, C4Portrait_None))
111 {
112 // no portrait
113 // nothing to be done here :D
114 }
115 else
116 {
117 // get portrait by info string
118 const char *szPortraitName; C4ID idPortraitID;
119 szPortraitName = C4Portrait::EvaluatePortraitString(szPortrait: PortraitFile, rIDOut&: idPortraitID, idDefaultID: id, pdwClrOut: nullptr);
120 // get portrait def
121 C4Def *pPortraitDef = Game.Defs.ID2Def(id: idPortraitID);
122 // def found?
123 if (pPortraitDef && pPortraitDef->Portraits)
124 {
125 // find portrait by name
126 C4DefGraphics *pDefPortraitGfx = pPortraitDef->Portraits->Get(szGrpName: szPortraitName);
127 C4PortraitGraphics *pPortraitGfx = pDefPortraitGfx ? pDefPortraitGfx->IsPortrait() : nullptr;
128 // link if found
129 if (pPortraitGfx)
130 Portrait.Link(pGfxPortrait: pPortraitGfx);
131 else
132 {
133 // portrait not found? Either the specification has been deleted (bad), or this is some scenario with custom portraits
134 // assume the latter, and temp-assign a random portrait for the duration of this round
135 if (Config.Graphics.AddNewCrewPortraits) SetRandomPortrait(idSourceDef: 0, fAssignPermanently: false, fCopyFile: false);
136 }
137 }
138 }
139 }
140 // portrait not defined or invalid (custom w/o file or invalid file)
141 // assign a new one (local players only)
142 if (!*PortraitFile && fLoadPortrait)
143 // try to load a custom portrait
144 if (!fPortraitFileChecked && Portrait.Load(rGrp&: hGroup, C4CFN_Portrait_Old, C4CFN_Portrait, C4CFN_PortraitOverlay))
145 // assign it as custom portrait
146 SCopy(C4Portrait_Custom, sTarget: PortraitFile);
147 else if (Config.Graphics.AddNewCrewPortraits)
148 // assign a new random crew portrait
149 SetRandomPortrait(idSourceDef: 0, fAssignPermanently: true, fCopyFile: false);
150
151 return true;
152}
153
154bool C4ObjectInfo::Save(C4Group &hGroup, bool fStoreTiny, C4DefList *pDefs)
155{
156 // Set group file name; rename if necessary
157 char szTempGroup[_MAX_PATH + 1];
158 SCopy(szSource: Name, sTarget: szTempGroup, _MAX_PATH);
159 MakeFilenameFromTitle(szTitle: szTempGroup);
160 SAppend(szSource: ".c4i", szTarget: szTempGroup, _MAX_PATH);
161 if (!SEqualNoCase(szStr1: Filename, szStr2: szTempGroup))
162 {
163 if (!Filename[0])
164 {
165 // first time creation of file - make sure it's not a duplicate
166 SCopy(szSource: szTempGroup, sTarget: Filename, _MAX_PATH);
167 while (hGroup.FindEntry(szWildCard: Filename))
168 {
169 // if a crew info of that name exists already, rename!
170 RemoveExtension(szFileName: Filename);
171 int32_t iFinNum = GetTrailingNumber(strString: Filename), iLen = SLen(sptr: Filename);
172 while (iLen && Inside(ival: Filename[iLen - 1], lbound: '0', rbound: '9')) --iLen;
173 if (iLen > _MAX_PATH - 22) { LogNTr(level: spdlog::level::err, fmt: "Error generating unique filename for {}({}): Path overflow", args: +Name, args: hGroup.GetFullName().getData()); break; }
174 *std::to_chars(first: Filename + iLen, last: Filename + std::size(Filename) - 1, value: iFinNum + 1).ptr = '\0';
175 EnforceExtension(szFileName: Filename, szExtension: "c4i");
176 }
177 }
178 else
179 {
180 // Crew was renamed; file rename necessary, if the name is not blocked by another crew info
181 if (!hGroup.FindEntry(szWildCard: szTempGroup))
182 if (hGroup.Rename(szFile: Filename, szNewName: szTempGroup))
183 SCopy(szSource: szTempGroup, sTarget: Filename, _MAX_PATH);
184 else
185 {
186 // could not rename. Not fatal; just use old file
187 LogNTr(level: spdlog::level::err, fmt: "Error adjusting crew info for {} into {}: Rename error from {} to {}!", args: +Name, args: hGroup.GetFullName().getData(), args: +Filename, args: +szTempGroup);
188 }
189 }
190 }
191 // Set temp group file name
192 SCopy(szSource: Filename, sTarget: szTempGroup);
193 MakeTempFilename(szFileName: szTempGroup);
194 // If object info group exists, copy to temp group file
195 hGroup.Extract(szFiles: Filename, szExtractTo: szTempGroup);
196 // Open temp group
197 C4Group hTemp;
198 if (!hTemp.Open(szGroupName: szTempGroup, fCreate: true))
199 return false;
200
201 // New portrait present, or old portrait not saved yet (old player begin updated)?
202 if (!fStoreTiny && Config.Graphics.SaveDefaultPortraits) if (pNewPortrait || (Config.Graphics.AddNewCrewPortraits && Portrait.GetGfx() && !hTemp.FindEntry(C4CFN_Portrait)))
203 {
204 C4Portrait *pSavePortrait = pNewPortrait ? pNewPortrait : &Portrait;
205 C4DefGraphics *pPortraitGfx;
206 // erase any old-style portrait
207 hGroup.Delete(C4CFN_Portrait_Old, fRecursive: false);
208 // save new
209 if (pSavePortrait->GetGfx()) pSavePortrait->SavePNG(rGroup&: hTemp, C4CFN_Portrait, C4CFN_PortraitOverlay);
210 // save spec
211 if (pNewPortrait)
212 {
213 // owned portrait?
214 if (pNewPortrait->IsOwnedGfx())
215 // use saved portrait
216 SCopy(C4Portrait_Custom, sTarget: PortraitFile);
217 else
218 {
219 if ((pPortraitGfx = pNewPortrait->GetGfx()) && pPortraitGfx->pDef)
220 {
221 // same ID: save portrait name only
222 if (pPortraitGfx->pDef->id == id)
223 SCopy(szSource: pPortraitGfx->GetName(), sTarget: PortraitFile);
224 else
225 {
226 // different ID (crosslinked portrait)
227 SCopy(szSource: C4IdText(id: pPortraitGfx->pDef->id), sTarget: PortraitFile);
228 SAppend(szSource: "::", szTarget: PortraitFile);
229 SAppend(szSource: pPortraitGfx->GetName(), szTarget: PortraitFile);
230 }
231 }
232 else
233 // No portrait
234 SCopy(C4Portrait_None, sTarget: PortraitFile);
235 }
236 }
237 // portrait synced
238 }
239
240 // delete default portraits if they are not desired
241 if (!fStoreTiny && !Config.Graphics.SaveDefaultPortraits && hTemp.FindEntry(C4CFN_Portrait))
242 {
243 if (!SEqual(szStr1: PortraitFile, C4Portrait_Custom))
244 {
245 hTemp.Delete(C4CFN_Portrait);
246 hTemp.Delete(C4CFN_PortraitOverlay);
247 }
248 }
249
250 // custom rank image present?
251 if (pDefs && !fStoreTiny)
252 {
253 C4Def *pDef = pDefs->ID2Def(id);
254 if (pDef)
255 {
256 if (pDef->pRankSymbols)
257 {
258 C4FacetExSurface fctRankSymbol;
259 if (C4RankSystem::DrawRankSymbol(fctSymbol: &fctRankSymbol, iRank: Rank, pfctRankSymbols: pDef->pRankSymbols, iRankSymbolCount: pDef->iNumRankSymbols, fOwnSurface: true))
260 {
261 fctRankSymbol.GetFace().SavePNG(hGroup&: hTemp, C4CFN_ClonkRank);
262 }
263 }
264 else
265 {
266 // definition does not have custom rank symbols: Remove any rank image from Clonk
267 hTemp.Delete(C4CFN_ClonkRank);
268 }
269 }
270 }
271
272 // Save info to temp group
273 if (!C4ObjectInfoCore::Save(hGroup&: hTemp, pDefs))
274 {
275 hTemp.Close(); return false;
276 }
277 // Close temp group
278 hTemp.Sort(C4FLS_Object);
279 if (!hTemp.Close())
280 return false;
281 // Move temp group to mother group
282 if (!hGroup.Move(szFile: szTempGroup, szAddAs: Filename))
283 return false;
284 // Success
285 return true;
286}
287
288void C4ObjectInfo::Evaluate()
289{
290 Retire();
291 if (WasInAction) Rounds++;
292}
293
294void C4ObjectInfo::Clear()
295{
296 Portrait.Clear();
297 delete pNewPortrait; pNewPortrait = nullptr;
298 delete pCustomPortrait; pCustomPortrait = nullptr;
299 pDef = nullptr;
300}
301
302void C4ObjectInfo::Draw(C4Facet &cgo, bool fShowPortrait, bool fCaptain, C4Object *pOfObj)
303{
304 const auto hideElements = pOfObj ? pOfObj->Def->HideHUDElements : 0;
305 int iX = 0;
306
307 // Portrait
308 if (fShowPortrait && !(hideElements & C4DefCore::HH_Portrait))
309 {
310 C4DefGraphics *pPortraitGfx;
311 if (pPortraitGfx = Portrait.GetGfx()) if (pPortraitGfx->Bitmap->Wdt)
312 {
313 C4Facet ccgo; ccgo.Set(nsfc: cgo.Surface, nx: cgo.X + iX, ny: cgo.Y, nwdt: 4 * cgo.Hgt / 3 + 10, nhgt: cgo.Hgt + 10);
314 uint32_t dwColor = 0xFFFFFFFF;
315 if (pOfObj && Game.Players.Get(iPlayer: pOfObj->Owner))
316 dwColor = Game.Players.Get(iPlayer: pOfObj->Owner)->ColorDw;
317 pPortraitGfx->DrawClr(cgo&: ccgo, fAspect: true, dwClr: dwColor);
318 iX += 4 * cgo.Hgt / 3;
319 }
320 }
321
322 // Captain symbol
323 if (fCaptain && !(hideElements & C4DefCore::HH_Captain))
324 {
325 Game.GraphicsResource.fctCaptain.Draw(sfcTarget: cgo.Surface, iX: cgo.X + iX, iY: cgo.Y, iPhaseX: 0, iPhaseY: 0);
326 iX += Game.GraphicsResource.fctCaptain.Wdt;
327 }
328
329 // Rank symbol
330 C4RankSystem *pRankSys = &Game.Rank;
331 C4FacetEx *pRankRes = &Game.GraphicsResource.fctRank;
332 int iRankCnt = Game.GraphicsResource.iNumRanks;
333 if (pOfObj)
334 {
335 C4Def *pDef = pOfObj->Def;
336 if (pDef->pRankSymbols)
337 {
338 pRankRes = pDef->pRankSymbols;
339 iRankCnt = pDef->iNumRankSymbols;
340 }
341 if (pDef->pRankNames)
342 {
343 pRankSys = pDef->pRankNames;
344 }
345 }
346 if (!(hideElements & C4DefCore::HH_RankImage))
347 {
348 pRankSys->DrawRankSymbol(fctSymbol: nullptr, iRank: Rank, pfctRankSymbols: pRankRes, iRankSymbolCount: iRankCnt, fOwnSurface: false, iXOff: iX, cgoDrawDirect: &cgo);
349 iX += Game.GraphicsResource.fctRank.Wdt;
350 }
351
352 std::string nameAndRank;
353 if (Rank > 0 && !(hideElements & C4DefCore::HH_Rank))
354 {
355 nameAndRank += sRankName.getData();
356 }
357 if (!(hideElements & C4DefCore::HH_Name))
358 {
359 if (!nameAndRank.empty())
360 {
361 nameAndRank += '|';
362 }
363 nameAndRank += pOfObj->GetName();
364 }
365 // Rank & Name
366 if (!nameAndRank.empty())
367 {
368 Application.DDraw->TextOut(szText: nameAndRank.c_str(),
369 rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + iX, iTy: cgo.Y, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ALeft);
370 }
371}
372
373void C4ObjectInfo::Recruit()
374{
375 // already recruited?
376 if (InAction) return;
377 WasInAction = true;
378 InAction = true;
379 InActionTime = Game.Time;
380 // rank name overload by def?
381 C4Def *pDef = Game.Defs.ID2Def(id);
382 if (pDef) if (pDef->pRankNames)
383 {
384 StdStrBuf sRank(pDef->pRankNames->GetRankName(iRank: Rank, fReturnLastIfOver: true));
385 if (sRank) sRankName.Copy(Buf2: sRank);
386 }
387}
388
389void C4ObjectInfo::Retire()
390{
391 // not recruited?
392 if (!InAction) return;
393 // retire
394 InAction = false;
395 TotalPlayingTime += (Game.Time - InActionTime);
396}
397
398bool C4ObjectInfo::SetRandomPortrait(C4ID idSourceDef, bool fAssignPermanently, bool fCopyFile)
399{
400 // No source def specified: use own def
401 if (!idSourceDef)
402 idSourceDef = id;
403 // Get source def
404 C4Def *pPortraitDef = Game.Defs.ID2Def(id: idSourceDef);
405 // Portrait source def not loaded: do not assign a portrait now, so the clonk can get
406 // the correct portrait later when the source def is available (assuming this clonk is
407 // not going to be used in this round anyway)
408 if (!pPortraitDef)
409 return false;
410 // Portrait def is loaded but does not have any portraits
411 if (!pPortraitDef->PortraitCount)
412 {
413 // Then use CLNK portraits (2do: base on include chains in latter case)?
414 pPortraitDef = Game.Defs.ID2Def(id: C4ID_Clonk);
415 // Assign permanently, assuming it is some kind of normal clonk here...
416 fAssignPermanently = true;
417 fCopyFile = false;
418 // No CLNK loaded or no portraits present? forget it, then
419 if (!pPortraitDef || !pPortraitDef->PortraitCount) return false;
420 }
421 // shouldn't happen if PortraitCount is != 0
422 if (!pPortraitDef->Portraits) return false;
423 // set a random portrait (note: not net synced!)
424 return SetPortrait(pNewPortraitGfx: pPortraitDef->Portraits->GetByIndex(iIndex: SafeRandom(range: pPortraitDef->PortraitCount)), fAssignPermanently, fCopyFile);
425}
426
427bool C4ObjectInfo::SetPortrait(const char *szPortraitName, C4Def *pSourceDef, bool fAssignPermanently, bool fCopyFile)
428{
429 // safety
430 if (!szPortraitName || !pSourceDef || !pSourceDef->Portraits) return false;
431 // Special case: custom portrait
432 if (SEqual(szStr1: szPortraitName, C4Portrait_Custom))
433 // does the info really have a custom portrait?
434 if (pCustomPortrait)
435 // set the custom portrait - we're just re-linking the custom portrait (fAssignPermanently or fCopyFile have nothing to do here)
436 return Portrait.Link(pGfxPortrait: pCustomPortrait->GetGfx());
437 // set desired portrait
438 return SetPortrait(pNewPortraitGfx: pSourceDef->Portraits->Get(szGrpName: szPortraitName), fAssignPermanently, fCopyFile);
439}
440
441bool C4ObjectInfo::SetPortrait(C4PortraitGraphics *pNewPortraitGfx, bool fAssignPermanently, bool fCopyFile)
442{
443 // safety
444 if (!pNewPortraitGfx) return false;
445 // assign portrait
446 if (fCopyFile) Portrait.CopyFrom(rCopyGfx&: *pNewPortraitGfx); else Portrait.Link(pGfxPortrait: pNewPortraitGfx);
447 // store permanently?
448 if (fAssignPermanently)
449 {
450 if (!pNewPortrait) pNewPortrait = new C4Portrait();
451 pNewPortrait->CopyFrom(rCopy&: Portrait);
452 }
453 // done, success
454 return true;
455}
456
457bool C4ObjectInfo::ClearPortrait(bool fPermanently)
458{
459 // no portrait
460 Portrait.Clear();
461 // clear new portrait; do not delete class (because empty class means no-portrait-as-new-setting)
462 if (fPermanently) if (pNewPortrait) pNewPortrait->Clear(); else pNewPortrait = new C4Portrait();
463 // done, success
464 return true;
465}
466