1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2020, 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/* Text messages drawn inside the viewport */
18
19#include <C4Include.h>
20#include <C4GameMessage.h>
21
22#include <C4Object.h>
23#include <C4Application.h>
24#include <C4Game.h>
25#include <C4Player.h>
26
27#include <algorithm>
28
29const int32_t TextMsgDelayFactor = 2; // frames per char message display time
30
31C4GameMessage::C4GameMessage() : pFrameDeco(nullptr) {}
32
33C4GameMessage::~C4GameMessage()
34{
35 delete pFrameDeco;
36}
37
38void C4GameMessage::Init(int32_t iType, const StdStrBuf &sText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, const char *szPortraitDef, uint32_t dwFlags, int width)
39{
40 // safety!
41 if (pTarget && !pTarget->Status) pTarget = nullptr;
42 // Set data
43 Text.Copy(Buf2: sText);
44 Target = pTarget;
45 X = iX; Y = iY; Wdt = width;
46 Player = iPlayer;
47 ColorDw = dwClr;
48 Type = iType;
49 Delay = std::max<int32_t>(a: C4GM_MinDelay, b: Text.getLength() * TextMsgDelayFactor);
50 DecoID = idDecoID;
51 this->dwFlags = dwFlags;
52 if (szPortraitDef && *szPortraitDef) PortraitDef.Copy(pnData: szPortraitDef); else PortraitDef.Clear();
53 // Permanent message
54 if ('@' == Text[0])
55 {
56 Delay = -1;
57 Text.Move(iFrom: 1, inSize: Text.getLength());
58 Text.Shrink(iShrink: 1);
59 }
60 // frame decoration
61 delete pFrameDeco; pFrameDeco = nullptr;
62 if (DecoID)
63 {
64 pFrameDeco = new C4GUI::FrameDecoration();
65 if (!pFrameDeco->SetByDef(DecoID))
66 {
67 delete pFrameDeco;
68 pFrameDeco = nullptr;
69 }
70 }
71}
72
73void C4GameMessage::Append(const char *szText, bool fNoDuplicates)
74{
75 // Check for duplicates
76 if (fNoDuplicates)
77 for (const char *pPos = Text.getData(); *pPos; pPos = SAdvancePast(szSPos: pPos, cPast: '|'))
78 if (SEqual2(szStr1: pPos, szStr2: szText))
79 return;
80 // Append new line
81 Text.AppendChar(cChar: '|');
82 Text.Append(pnData: szText);
83 Delay += SLen(sptr: szText) * TextMsgDelayFactor;
84}
85
86bool C4GameMessage::Execute()
87{
88 // Delay / removal
89 if (Delay > 0) Delay--;
90 if (Delay == 0) return false;
91 // Done
92 return true;
93}
94
95int32_t DrawMessageOffset = -35; // For finding the optimum place to draw startup info & player messages...
96int32_t PortraitWidth = 64;
97int32_t PortraitIndent = 10;
98
99void C4GameMessage::Draw(C4FacetEx &cgo, int32_t iPlayer)
100{
101 int32_t alignment = dwFlags & C4GM_ALeft ? ALeft : dwFlags & C4GM_ACenter ? ACenter : dwFlags & C4GM_ARight ? ARight : -1;
102
103 // Globals or player
104 if (Type == C4GM_Global || ((Type == C4GM_GlobalPlayer) && (iPlayer == Player)))
105 {
106 int32_t iTextWdt, iTextHgt;
107 StdStrBuf sText;
108 int32_t x, y, wdt;
109 if (dwFlags & C4GM_XRel) x = X * cgo.Wdt / 100; else x = X;
110 if (dwFlags & C4GM_YRel) y = Y * cgo.Hgt / 100; else y = Y;
111 if (dwFlags & C4GM_WidthRel) wdt = Wdt * cgo.Wdt / 100; else wdt = Wdt;
112 if (~dwFlags & C4GM_NoBreak)
113 {
114 // Word wrap to cgo width
115 if (PortraitDef)
116 {
117 if (!wdt) wdt = BoundBy<int32_t>(bval: cgo.Wdt / 2, lbound: 50, rbound: std::min<int32_t>(a: 500, b: cgo.Wdt - 10));
118 int32_t iUnbrokenTextWidth = Game.GraphicsResource.FontRegular.GetTextWidth(szText: Text.getData(), fCheckMarkup: true);
119 wdt = std::min<int32_t>(a: wdt, b: iUnbrokenTextWidth + 10);
120 }
121 else
122 {
123 if (!wdt)
124 wdt = BoundBy<int32_t>(bval: cgo.Wdt - 50, lbound: 50, rbound: 500);
125 else
126 wdt = BoundBy<int32_t>(bval: wdt, lbound: 10, rbound: cgo.Wdt - 10);
127 }
128 iTextWdt = wdt;
129 iTextHgt = Game.GraphicsResource.FontRegular.BreakMessage(szMsg: Text.getData(), iWdt: iTextWdt, pOut: &sText, fCheckMarkup: true);
130 }
131 else
132 {
133 Game.GraphicsResource.FontRegular.GetTextExtent(szText: Text.getData(), rsx&: iTextWdt, rsy&: iTextHgt, fCheckMarkup: true);
134 sText.Ref(Buf2: Text);
135 }
136 int32_t iDrawX = cgo.X + x;
137 int32_t iDrawY = cgo.Y + y;
138
139 // draw message
140 if (PortraitDef)
141 {
142 // message with portrait
143 // bottom-placed portrait message: Y-Positioning 0 refers to bottom of viewport
144 if (dwFlags & C4GM_Bottom) iDrawY += cgo.Hgt;
145 else if (dwFlags & C4GM_VCenter) iDrawY += cgo.Hgt / 2;
146 if (dwFlags & C4GM_Right) iDrawX += cgo.Wdt;
147 else if (dwFlags & C4GM_HCenter) iDrawX += cgo.Wdt / 2;
148 // draw decoration
149 if (pFrameDeco)
150 {
151 C4Rect rect(iDrawX - cgo.TargetX, iDrawY - cgo.TargetY, iTextWdt + PortraitWidth + PortraitIndent + pFrameDeco->iBorderLeft + pFrameDeco->iBorderRight, (std::max)(a: iTextHgt, b: PortraitWidth) + pFrameDeco->iBorderTop + pFrameDeco->iBorderBottom);
152 if (dwFlags & C4GM_Bottom) { rect.y -= rect.Hgt; iDrawY -= rect.Hgt; }
153 else if (dwFlags & C4GM_VCenter) { rect.y -= rect.Hgt / 2; iDrawY -= rect.Hgt / 2; }
154 if (dwFlags & C4GM_Right) { rect.x -= rect.Wdt; iDrawX -= rect.Wdt; }
155 else if (dwFlags & C4GM_HCenter) { rect.x -= rect.Wdt / 2; iDrawX -= rect.Wdt / 2; }
156 pFrameDeco->Draw(cgo, rcDrawArea&: rect);
157 iDrawX += pFrameDeco->iBorderLeft;
158 iDrawY += pFrameDeco->iBorderTop;
159 }
160 else
161 iDrawY -= iTextHgt;
162 // draw portrait
163 C4FacetExSurface fctPortrait;
164 Game.DrawTextSpecImage(fctTarget&: fctPortrait, szSpec: PortraitDef.getData());
165 C4Facet facet(cgo.Surface, iDrawX, iDrawY, PortraitWidth, PortraitWidth);
166 fctPortrait.Draw(cgo&: facet);
167 // draw message
168 Application.DDraw->TextOut(szText: sText.getData(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: iDrawX + PortraitWidth + PortraitIndent, iTy: iDrawY, dwFCol: ColorDw, byForm: alignment != -1 ? alignment : ALeft);
169 }
170 else
171 {
172 // message without portrait
173 iDrawX += cgo.Wdt / 2;
174 iDrawY += 2 * cgo.Hgt / 3 + 50;
175 if (!(dwFlags & C4GM_Bottom)) iDrawY += DrawMessageOffset;
176 Application.DDraw->TextOut(szText: sText.getData(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: iDrawX, iTy: iDrawY, dwFCol: ColorDw, byForm: alignment != -1 ? alignment : ACenter);
177 }
178 }
179 // Positioned
180 else if (Type == C4GM_Target || ((Type == C4GM_TargetPlayer) && (iPlayer == Player)))
181 {
182 // adjust position by object; care about parallaxity
183 int32_t iMsgX, iMsgY;
184 if (Type == C4GM_Target || Type == C4GM_TargetPlayer)
185 {
186 Target->GetViewPos(riX&: iMsgX, riY&: iMsgY, tx: cgo.TargetX, ty: cgo.TargetY, fctViewport: cgo);
187 iMsgY -= Target->Def->Shape.Hgt / 2 + 5;
188 iMsgX += X; iMsgY += Y;
189 }
190 else
191 {
192 iMsgX = X; iMsgY = Y;
193 }
194 // check output bounds
195 if (Inside(ival: iMsgX - cgo.TargetX, lbound: 0, rbound: cgo.Wdt - 1))
196 if (Inside(ival: iMsgY - cgo.TargetY, lbound: 0, rbound: cgo.Hgt - 1))
197 {
198 // if the message is attached to an object and the object
199 // is invisible for that player, the message won't be drawn
200 if (Type == C4GM_Target)
201 if (!Target->IsVisible(iForPlr: iPlayer, fAsOverlay: false))
202 return;
203 // check fog of war
204 C4Player *pPlr = Game.Players.Get(iPlayer);
205 if (pPlr && pPlr->fFogOfWar)
206 if (!pPlr->FoWIsVisible(x: iMsgX, y: iMsgY))
207 {
208 // special: Target objects that ignore FoW should display the message even if within FoW
209 if (Type != C4GM_Target && Type != C4GM_TargetPlayer) return;
210 if (~Target->Category & C4D_IgnoreFoW) return;
211 }
212 // Word wrap to cgo width
213 StdStrBuf sText;
214 if (~dwFlags & C4GM_NoBreak)
215 Game.GraphicsResource.FontRegular.BreakMessage(szMsg: Text.getData(), iWdt: BoundBy<int32_t>(bval: cgo.Wdt, lbound: 50, rbound: 200), pOut: &sText, fCheckMarkup: true);
216 else
217 sText.Ref(Buf2: Text);
218 // Adjust position by output boundaries
219 int32_t iTX, iTY, iTWdt, iTHgt;
220 Game.GraphicsResource.FontRegular.GetTextExtent(szText: sText.getData(), rsx&: iTWdt, rsy&: iTHgt, fCheckMarkup: true);
221 iTX = BoundBy<int32_t>(bval: iMsgX - cgo.TargetX, lbound: iTWdt / 2, rbound: cgo.Wdt - iTWdt / 2);
222 iTY = BoundBy<int32_t>(bval: iMsgY - cgo.TargetY - iTHgt, lbound: 0, rbound: cgo.Hgt - iTHgt);
223 // Draw
224 Application.DDraw->TextOut(szText: sText.getData(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0,
225 sfcDest: cgo.Surface,
226 iTx: cgo.X + iTX,
227 iTy: cgo.Y + iTY,
228 dwFCol: ColorDw, byForm: alignment != -1 ? alignment : ACenter);
229 }
230 }
231}
232
233void C4GameMessage::UpdateDef(C4ID idUpdDef)
234{
235 // frame deco might be using updated/deleted def
236 if (pFrameDeco)
237 {
238 if (!pFrameDeco->UpdateGfx())
239 {
240 delete pFrameDeco;
241 pFrameDeco = nullptr;
242 }
243 }
244}
245
246C4GameMessageList::C4GameMessageList()
247{
248 Clear();
249}
250
251C4GameMessageList::~C4GameMessageList()
252{
253 Clear();
254}
255
256void C4GameMessageList::ClearPointers(C4Object *pObj)
257{
258 Messages.erase(first: std::remove_if(first: Messages.begin(), last: Messages.end(), pred: [pObj](const auto &msg)
259 {
260 return msg->Target == pObj;
261 }), last: Messages.end());
262}
263
264void C4GameMessageList::Clear()
265{
266 Messages.clear();
267}
268
269void C4GameMessageList::Execute()
270{
271 Messages.erase(first: std::remove_if(first: Messages.begin(), last: Messages.end(), pred: [](const auto &msg)
272 {
273 return !msg->Execute();
274 }), last: Messages.end());
275}
276
277bool C4GameMessageList::New(int32_t iType, const char *szText,
278 C4Object *pTarget, int32_t iPlayer,
279 int32_t iX, int32_t iY,
280 uint8_t bCol)
281{
282 return New(iType, Text: StdStrBuf::MakeRef(str: szText), pTarget, iPlayer, iX, iY, dwClr: 0xff000000 | Application.DDraw->Pal.GetClr(byCol: FColors[bCol]));
283}
284
285bool C4GameMessageList::New(int32_t iType, const char *szText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, const char *szPortraitDef, uint32_t dwFlags, int32_t width)
286{
287 return New(iType, Text: StdStrBuf::MakeRef(str: szText), pTarget, iPlayer, iX, iY, dwClr, idDecoID, szPortraitDef, dwFlags, width);
288}
289
290bool C4GameMessageList::New(int32_t iType, const StdStrBuf &sText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, const char *szPortraitDef, uint32_t dwFlags, int32_t width)
291{
292 if (!(dwFlags & C4GM_Multiple))
293 {
294 // Clear messages with same target
295 if (pTarget) ClearPointers(pObj: pTarget);
296
297 // Clear other player messages
298 if (iType == C4GM_Global || iType == C4GM_GlobalPlayer) ClearPlayers(iPlayer, dwPositioningFlags: dwFlags & C4GM_PositioningFlags);
299 }
300
301 // Object deleted?
302 if (pTarget && !pTarget->Status) return false;
303
304 // Empty message? (only deleting old message)
305 if (!sText.getLength()) return true;
306
307 // Add new message
308 C4GameMessage *msgNew = new C4GameMessage;
309 msgNew->Init(iType, sText, pTarget, iPlayer, iX, iY, dwClr, idDecoID, szPortraitDef, dwFlags, width);
310 Messages.emplace_back(args&: msgNew);
311
312 return true;
313}
314
315bool C4GameMessageList::Append(int32_t iType, const char *szText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint8_t bCol, bool fNoDuplicates)
316{
317 if (const auto &msg = std::find_if(first: Messages.begin(), last: Messages.end(), pred: [iType, pTarget, iPlayer](const auto &msg)
318 {
319 return (iType == C4GM_Target && msg->Target == pTarget)
320 || ((iType == C4GM_Global || iType == C4GM_GlobalPlayer) && msg->Player == iPlayer);
321 }); msg != Messages.end() && (*msg)->Target == pTarget)
322 {
323 (*msg)->Append(szText, fNoDuplicates);
324 }
325 else
326 {
327 New(iType, szText, pTarget, iPlayer, iX, iY, bCol);
328 }
329 return true;
330}
331
332void C4GameMessageList::ClearPlayers(int32_t iPlayer, int32_t dwPositioningFlags)
333{
334 Messages.erase(first: std::remove_if(first: Messages.begin(), last: Messages.end(), pred: [iPlayer, dwPositioningFlags](const auto &msg)
335 {
336 return msg->Player == iPlayer && msg->GetPositioningFlags() == dwPositioningFlags;
337 }), last: Messages.end());
338}
339
340void C4GameMessageList::UpdateDef(C4ID idUpdDef)
341{
342 for (const auto &it : Messages)
343 {
344 it->UpdateDef(idUpdDef);
345 }
346}
347
348void C4GameMessageList::Draw(C4FacetEx &cgo, int32_t iPlayer)
349{
350 for (const auto &it : Messages)
351 {
352 it->Draw(cgo, iPlayer);
353 }
354}
355