1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 1998-2000, Matthes Bender
5  * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
6  * Copyright (c) 2009-2016, The OpenClonk 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 /* Text messages drawn inside the viewport */
19 
20 #include "C4Include.h"
21 #include "gui/C4GameMessage.h"
22 
23 #include "graphics/C4Draw.h"
24 #include "graphics/C4GraphicsResource.h"
25 #include "object/C4Def.h"
26 #include "object/C4Object.h"
27 #include "player/C4Player.h"
28 #include "player/C4PlayerList.h"
29 
30 const int32_t ObjectMsgDelayFactor = 2, GlobalMsgDelayFactor = 3; // frames per char message display time
31 
32 C4GameMessage::C4GameMessage() = default;
33 
~C4GameMessage()34 C4GameMessage::~C4GameMessage()
35 {
36 	delete pFrameDeco;
37 }
38 
Init(int32_t iType,const StdStrBuf & sText,C4Object * pTarget,int32_t iPlayer,int32_t iX,int32_t iY,uint32_t dwClr,C4ID idDecoID,C4PropList * pSrc,uint32_t dwFlags,int width)39 void C4GameMessage::Init(int32_t iType, const StdStrBuf & sText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, C4PropList *pSrc, uint32_t dwFlags, int width)
40 {
41 	// safety!
42 	if (pTarget && !pTarget->Status) pTarget = nullptr;
43 	// Set data
44 	Text.Copy(sText);
45 	Target=pTarget;
46 	X=iX; Y=iY; Wdt=width; Hgt=0;
47 	Player=iPlayer;
48 	ColorDw=dwClr;
49 	Type=iType;
50 	Delay=std::max<int32_t>(C4GM_MinDelay, Text.getLength() * (Target ? ObjectMsgDelayFactor : GlobalMsgDelayFactor));
51 	DecoID=idDecoID;
52 	this->dwFlags=dwFlags;
53 	PictureDef=nullptr;
54 	PictureDefVal.Set0();
55 	if (pSrc)
56 	{
57 		// retain special width/height properties when using a message box on an object-local message
58 		if (Target)
59 		{
60 			C4Value val;
61 			if (pSrc->GetProperty(P_Wdt, &val))
62 				Wdt = val.getInt();
63 			if (pSrc->GetProperty(P_Hgt, &val))
64 				Hgt = val.getInt();
65 		}
66 
67 		// retain object or definition from the proplist
68 		PictureDef = pSrc->GetObject();
69 		if (!PictureDef) PictureDef = pSrc->GetDef();
70 		if (!PictureDef && pSrc->GetPropertyPropList(P_Source))
71 		{
72 			PictureDef = pSrc;
73 			PictureDefVal.SetPropList(pSrc);
74 		}
75 	}
76 	// Permanent message
77 	if ('@' == Text[0])
78 	{
79 		Delay=-1;
80 		Text.Move(1, Text.getLength());
81 		Text.Shrink(1);
82 	}
83 	// frame decoration
84 	delete pFrameDeco; pFrameDeco = nullptr;
85 	if (DecoID)
86 	{
87 		pFrameDeco = new C4GUI::FrameDecoration();
88 		if (!pFrameDeco->SetByDef(DecoID))
89 		{
90 			delete pFrameDeco;
91 			pFrameDeco = nullptr;
92 		}
93 	}
94 }
95 
Append(const char * szText,bool fNoDuplicates)96 void C4GameMessage::Append(const char *szText, bool fNoDuplicates)
97 {
98 	// Check for duplicates
99 	if (fNoDuplicates)
100 		for (const char *pPos = Text.getData(); *pPos; pPos = SAdvancePast(pPos, '|'))
101 			if (SEqual2(pPos, szText))
102 				return;
103 	// Append new line
104 	Text.AppendFormat("|%s", szText);
105 	Delay += SLen(szText) * (Target ? ObjectMsgDelayFactor : GlobalMsgDelayFactor);
106 }
107 
Execute()108 bool C4GameMessage::Execute()
109 {
110 	// Delay / removal
111 	if (Delay>0) Delay--;
112 	if (Delay==0) return false;
113 	// Done
114 	return true;
115 }
116 
117 int32_t DrawMessageOffset = -35; // For finding the optimum place to draw startup info & player messages...
118 int32_t PictureWidth = 64;
119 int32_t PictureIndent = 10;
120 
Draw(C4TargetFacet & cgo,int32_t iPlayer)121 void C4GameMessage::Draw(C4TargetFacet &cgo, int32_t iPlayer)
122 {
123 	// Globals or player
124 	if (Type == C4GM_Global || ((Type == C4GM_GlobalPlayer) && (iPlayer == Player)))
125 	{
126 		int32_t iTextWdt,iTextHgt;
127 		StdStrBuf sText;
128 		int32_t x,y,wdt;
129 		if (dwFlags & C4GM_XRel) x = X * cgo.Wdt / 100; else x = X;
130 		if (dwFlags & C4GM_YRel) y = Y * cgo.Hgt / 100; else y = Y;
131 		if (dwFlags & C4GM_WidthRel) wdt = Wdt * cgo.Wdt / 100; else wdt = Wdt;
132 		if (~dwFlags & C4GM_NoBreak)
133 		{
134 			// Word wrap to cgo width
135 			if (PictureDef)
136 			{
137 				if (!wdt) wdt = Clamp<int32_t>(cgo.Wdt/2, 50, std::min<int32_t>(500, cgo.Wdt-10));
138 				int32_t iUnbrokenTextWidth = ::GraphicsResource.FontRegular.GetTextWidth(Text.getData(), true);
139 				wdt = std::min<int32_t>(wdt, iUnbrokenTextWidth+10);
140 			}
141 			else
142 			{
143 				if (!wdt)
144 					wdt = Clamp<int32_t>(cgo.Wdt-50, 50, 500);
145 				else
146 					wdt = Clamp<int32_t>(wdt, 10, cgo.Wdt-10);
147 			}
148 			iTextWdt = wdt * cgo.Zoom;
149 			iTextHgt = ::GraphicsResource.FontRegular.BreakMessage(Text.getData(), iTextWdt, &sText, true);
150 		}
151 		else
152 		{
153 			::GraphicsResource.FontRegular.GetTextExtent(Text.getData(), iTextWdt, iTextHgt, true);
154 			sText.Ref(Text);
155 		}
156 		int32_t iDrawX = cgo.X+x;
157 		int32_t iDrawY = cgo.Y+y;
158 		// draw message
159 		if (PictureDef)
160 		{
161 			// message with portrait
162 			// bottom-placed portrait message: Y-Positioning 0 refers to bottom of viewport
163 			if (dwFlags & C4GM_Bottom) iDrawY += cgo.Hgt;
164 			else if (dwFlags & C4GM_VCenter) iDrawY += cgo.Hgt/2;
165 			if (dwFlags & C4GM_Right) iDrawX += cgo.Wdt;
166 			else if (dwFlags & C4GM_HCenter) iDrawX += cgo.Wdt/2;
167 			// draw decoration
168 			if (pFrameDeco)
169 			{
170 				iDrawX *= cgo.Zoom; iDrawY *= cgo.Zoom;
171 				C4Rect rect(iDrawX-cgo.TargetX, iDrawY-cgo.TargetY, iTextWdt + PictureWidth + PictureIndent + pFrameDeco->iBorderLeft + pFrameDeco->iBorderRight, std::max(iTextHgt, PictureWidth) + pFrameDeco->iBorderTop + pFrameDeco->iBorderBottom);
172 				if (dwFlags & C4GM_Bottom) { rect.y -= rect.Hgt; iDrawY -= rect.Hgt; }
173 				else if (dwFlags & C4GM_VCenter) { rect.y -= rect.Hgt/2; iDrawY -= rect.Hgt/2; }
174 				if (dwFlags & C4GM_Right) { rect.x -= rect.Wdt; iDrawX -= rect.Wdt; }
175 				else if (dwFlags & C4GM_HCenter) { rect.x -= rect.Wdt/2; iDrawX -= rect.Wdt/2; }
176 				pFrameDeco->Draw(cgo, rect);
177 				iDrawX += pFrameDeco->iBorderLeft;
178 				iDrawY += pFrameDeco->iBorderTop;
179 			}
180 			else
181 				iDrawY -= iTextHgt;
182 			// draw picture
183 			// can only be def or object because has been checked on assignment
184 
185 			C4Facet facet(cgo.Surface, iDrawX, iDrawY, PictureWidth, PictureWidth);
186 			if(PictureDef->GetObject())
187 				PictureDef->GetObject()->DrawPicture(facet);
188 			else if (PictureDef->GetDef())
189 				PictureDef->GetDef()->Draw(facet);
190 			else
191 				Game.DrawPropListSpecImage(facet, PictureDef);
192 
193 			// draw message
194 			pDraw->TextOut(sText.getData(),::GraphicsResource.FontRegular,1.0,cgo.Surface,iDrawX+PictureWidth+PictureIndent,iDrawY,ColorDw,ALeft);
195 		}
196 		else
197 		{
198 			// message without picture
199 			iDrawX += (cgo.Wdt/2) * cgo.Zoom;
200 			iDrawY += (2 * cgo.Hgt / 3 + 50) * cgo.Zoom;
201 			if (!(dwFlags & C4GM_Bottom)) iDrawY += DrawMessageOffset;
202 			pDraw->TextOut(sText.getData(),::GraphicsResource.FontRegular,1.0,cgo.Surface,iDrawX,iDrawY,ColorDw,ACenter);
203 		}
204 	}
205 	// Positioned
206 	else if (Type == C4GM_Target || ((Type == C4GM_TargetPlayer) && (iPlayer == Player)))
207 	{
208 		// adjust position by object; care about parallaxity
209 		float iMsgX, iMsgY, newzoom;
210 		Target->GetDrawPosition(cgo, iMsgX, iMsgY, newzoom);
211 		if(~dwFlags & C4GM_YRel)
212 			iMsgY -= Target->Def->Shape.Hgt/2+5;
213 		iMsgX+=X; iMsgY+=Y;
214 		// check output bounds
215 		if (Inside<float>((iMsgX - cgo.X) * newzoom, 0, cgo.Wdt * cgo.Zoom - 1) || (dwFlags & C4GM_XRel))
216 			if (Inside<float>((iMsgY - cgo.Y) * newzoom, 0, cgo.Hgt * cgo.Zoom - 1) || (dwFlags & C4GM_YRel))
217 			{
218 				// if the message is attached to an object and the object
219 				// is invisible for that player, the message won't be drawn
220 				if (Type == C4GM_Target)
221 					if (!Target->IsVisible(iPlayer, false))
222 						return;
223 				// Word wrap to cgo width
224 				StdStrBuf sText;
225 				if (~dwFlags & C4GM_NoBreak)
226 				{
227 					// standard break width
228 					int breakWdt = Clamp<int32_t>(cgo.Wdt * cgo.Zoom, 50, 200);
229 
230 					// user supplied width?
231 					if (Wdt)
232 						breakWdt = Wdt;
233 
234 					::GraphicsResource.FontRegular.BreakMessage(Text.getData(), breakWdt, &sText, true);
235 				}
236 				else
237 					sText.Ref(Text);
238 
239 				// vertical placement
240 				if (dwFlags & C4GM_Bottom)
241 					iMsgY += Hgt; // iTHgt will be substracted below
242 				else if (dwFlags & C4GM_Top)
243 					;
244 				else if (dwFlags & C4GM_VCenter)
245 					iMsgY += Hgt / 2;
246 
247 				// horizontal placement
248 				int alignment = ACenter;
249 
250 				if (dwFlags & C4GM_Left)
251 					alignment = ALeft;
252 				else if (dwFlags & C4GM_Right)
253 				{
254 					alignment = ARight;
255 					iMsgX += Wdt;
256 				}
257 				else if (dwFlags & C4GM_HCenter)
258 					iMsgX += Wdt / 2;
259 
260 				// calculate display position and adjust position by output boundaries
261 				float iTX, iTY;
262 				iTX = (iMsgX - cgo.X) * newzoom;
263 				iTY = (iMsgY - cgo.Y) * newzoom;
264 				int iTWdt, iTHgt;
265 				::GraphicsResource.FontRegular.GetTextExtent(sText.getData(),iTWdt,iTHgt,true);
266 
267 				// adjust zoom if needed
268 				float zoom = 1.0;
269 				if(dwFlags & C4GM_Zoom)
270 					zoom = cgo.Zoom;
271 
272 				if (dwFlags & C4GM_Bottom)
273 					iTY -= zoom * float(iTHgt);
274 				else if (dwFlags & C4GM_VCenter)
275 					iTY -= zoom * float(iTHgt/2);
276 				else if (~dwFlags & C4GM_Top)
277 					iTY -= zoom * float(iTHgt); // above object is standard
278 
279 				if (dwFlags & C4GM_Right)
280 					iTX += 0.25f * float(iTWdt) * (1.0f - zoom);
281 
282 				// adjustment for objects at border of screen?
283 				// +0.5f for proper rounding; avoids oscillations near pixel border:
284 				if (~dwFlags & C4GM_XRel) iTX = Clamp<float>(iTX, iTWdt/2, cgo.Wdt * cgo.Zoom - iTWdt / 2) + 0.5f;
285 				if (~dwFlags & C4GM_YRel) iTY = Clamp<float>(iTY, 0, cgo.Hgt * cgo.Zoom - iTHgt) + 0.5f;
286 
287 				// Draw
288 				pDraw->TextOut(sText.getData(), ::GraphicsResource.FontRegular, zoom,
289 				                           cgo.Surface,
290 				                           cgo.X + iTX,
291 				                           cgo.Y + iTY,
292 				                           ColorDw, alignment);
293 				return;
294 			}
295 	}
296 }
297 
UpdateDef(C4ID idUpdDef)298 void C4GameMessage::UpdateDef(C4ID idUpdDef)
299 {
300 	// frame deco might be using updated/deleted def
301 	if (pFrameDeco)
302 	{
303 		if (!pFrameDeco->UpdateGfx())
304 		{
305 			delete pFrameDeco;
306 			pFrameDeco = nullptr;
307 		}
308 	}
309 }
310 
C4GameMessageList()311 C4GameMessageList::C4GameMessageList()
312 {
313 	Default();
314 }
315 
~C4GameMessageList()316 C4GameMessageList::~C4GameMessageList()
317 {
318 	Clear();
319 }
320 
Default()321 void C4GameMessageList::Default()
322 {
323 	First=nullptr;
324 }
325 
ClearPointers(C4Object * pObj)326 void C4GameMessageList::ClearPointers(C4Object *pObj)
327 {
328 	C4GameMessage *cmsg,*next,*prev=nullptr;
329 	for (cmsg=First; cmsg; cmsg=next)
330 	{
331 		next=cmsg->Next;
332 		if (cmsg->Target==pObj)
333 			{ delete cmsg; if (prev) prev->Next=next; else First=next; }
334 		else
335 			prev=cmsg;
336 	}
337 }
338 
Clear()339 void C4GameMessageList::Clear()
340 {
341 	C4GameMessage *cmsg,*next;
342 	for (cmsg=First; cmsg; cmsg=next)
343 	{
344 		next=cmsg->Next;
345 		delete cmsg;
346 	}
347 	First=nullptr;
348 }
349 
Execute()350 void C4GameMessageList::Execute()
351 {
352 	C4GameMessage *cmsg,*next,*prev=nullptr;
353 	for (cmsg=First; cmsg; cmsg=next)
354 	{
355 		next=cmsg->Next;
356 		if (!cmsg->Execute())
357 			{ delete cmsg; if (prev) prev->Next=next; else First=next; }
358 		else
359 			prev=cmsg;
360 	}
361 }
362 
New(int32_t iType,const char * szText,C4Object * pTarget,int32_t iPlayer,int32_t iX,int32_t iY,uint32_t dwClr,C4ID idDecoID,C4PropList * pSrc,uint32_t dwFlags,int32_t width)363 bool C4GameMessageList::New(int32_t iType, const char *szText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, C4PropList *pSrc, uint32_t dwFlags, int32_t width)
364 {
365 	return New(iType, StdStrBuf(szText), pTarget, iPlayer, iX, iY, dwClr, idDecoID, pSrc, dwFlags, width);
366 }
367 
New(int32_t iType,const StdStrBuf & sText,C4Object * pTarget,int32_t iPlayer,int32_t iX,int32_t iY,uint32_t dwClr,C4ID idDecoID,C4PropList * pSrc,uint32_t dwFlags,int32_t width)368 bool C4GameMessageList::New(int32_t iType, const StdStrBuf & sText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t dwClr, C4ID idDecoID, C4PropList *pSrc, uint32_t dwFlags, int32_t width)
369 {
370 	if (!(dwFlags & C4GM_Multiple))
371 	{
372 		// Clear messages with same target
373 		if (pTarget) ClearPointers(pTarget);
374 
375 		// Clear other global messages for the given player
376 		if (iType == C4GM_Global || iType == C4GM_GlobalPlayer) ClearPlayers(iPlayer, dwFlags & C4GM_PositioningFlags);
377 	}
378 
379 	// Object deleted?
380 	if (pTarget && !pTarget->Status) return false;
381 
382 	// Empty message? (only deleting old message)
383 	if (!sText.getLength()) return true;
384 
385 	// Add new message
386 	C4GameMessage *msgNew = new C4GameMessage;
387 	msgNew->Init(iType, sText,pTarget,iPlayer,iX,iY,dwClr, idDecoID, pSrc, dwFlags, width);
388 	msgNew->Next=First;
389 	First=msgNew;
390 
391 	return true;
392 }
393 
Append(int32_t iType,const char * szText,C4Object * pTarget,int32_t iPlayer,int32_t iX,int32_t iY,uint32_t bCol,bool fNoDuplicates)394 bool C4GameMessageList::Append(int32_t iType, const char *szText, C4Object *pTarget, int32_t iPlayer, int32_t iX, int32_t iY, uint32_t bCol, bool fNoDuplicates)
395 {
396 	C4GameMessage *cmsg = nullptr;
397 	if (iType == C4GM_Target)
398 	{
399 		for (cmsg=::Messages.First; cmsg; cmsg=cmsg->Next)
400 			if (pTarget == cmsg->Target)
401 				break;
402 	}
403 	if (iType == C4GM_Global || iType == C4GM_GlobalPlayer)
404 	{
405 		for (cmsg=::Messages.First; cmsg; cmsg=cmsg->Next)
406 			if (iPlayer == cmsg->Player)
407 				break;
408 	}
409 	if (!cmsg || pTarget!=cmsg->Target)
410 	{
411 		New(iType, szText, pTarget, iPlayer, iX, iY, bCol);
412 	}
413 	else
414 	{
415 		cmsg->Append(szText, fNoDuplicates);
416 	}
417 	return true;
418 }
419 
ClearPlayers(int32_t iPlayer,int32_t dwPositioningFlags)420 void C4GameMessageList::ClearPlayers(int32_t iPlayer, int32_t dwPositioningFlags)
421 {
422 	C4GameMessage *cmsg,*next,*prev=nullptr;
423 	for (cmsg=First; cmsg; cmsg=next)
424 	{
425 		next=cmsg->Next;
426 		if ( (cmsg->Type == C4GM_Global || cmsg->Type == C4GM_GlobalPlayer) && cmsg->Player==iPlayer && cmsg->GetPositioningFlags() == dwPositioningFlags)
427 			{ delete cmsg; if (prev) prev->Next=next; else First=next; }
428 		else
429 			prev=cmsg;
430 	}
431 }
432 
UpdateDef(C4ID idUpdDef)433 void C4GameMessageList::UpdateDef(C4ID idUpdDef)
434 {
435 	C4GameMessage *cmsg;
436 	for (cmsg=First; cmsg; cmsg=cmsg->Next) cmsg->UpdateDef(idUpdDef);
437 }
438 
Draw(C4TargetFacet & gui_cgo,C4TargetFacet & cgo,int32_t iPlayer)439 void C4GameMessageList::Draw(C4TargetFacet &gui_cgo, C4TargetFacet &cgo, int32_t iPlayer)
440 {
441 	C4GameMessage *cmsg;
442 	for (cmsg=First; cmsg; cmsg=cmsg->Next)
443 	{
444 		if ((cmsg->Target && (cmsg->Target->Category & C4D_Foreground)) || cmsg->Type == C4GM_Global || cmsg->Type == C4GM_GlobalPlayer)
445 			cmsg->Draw(gui_cgo,iPlayer);
446 		else
447 			cmsg->Draw(cgo,iPlayer);
448 	}
449 }
450 
GameMsgObjectError(const char * szText,C4Object * pTarget,bool Red)451 void GameMsgObjectError(const char *szText, C4Object *pTarget, bool Red)
452 {
453 	::Messages.New(C4GM_TargetPlayer,szText,pTarget,pTarget->Controller,0,0, Red ? C4RGB(0xca, 0, 0) : C4RGB(0xff, 0xff, 0xff));
454 }
455 
456 C4GameMessageList Messages;
457