1/*
2 * LegacyClonk
3 *
4 * Copyright (C) 1998-2000, Matthes Bender (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/* Fullscreen startup log and chat type-in */
18
19#include <C4Include.h>
20#include <C4MessageBoard.h>
21
22#include <C4Object.h>
23#include <C4Application.h>
24#include <C4LoaderScreen.h>
25#include <C4Gui.h>
26#include <C4Console.h>
27#include <C4Network2Dialogs.h>
28#include <C4Player.h>
29
30const int C4LogSize = 30000, C4LogMaxLines = 1000;
31
32C4MessageBoard::C4MessageBoard() : LogBuffer(C4LogSize, C4LogMaxLines, 0, " ", false)
33{
34 Default();
35}
36
37C4MessageBoard::~C4MessageBoard()
38{
39 Clear();
40}
41
42void C4MessageBoard::Default()
43{
44 Clear();
45 Active = false;
46 Delay = -1;
47 Fader = 0;
48 Speed = 2;
49 Output.Default();
50 Startup = false;
51 Empty = true;
52 ScreenFader = 0;
53 iMode = 0;
54 iLines = 4;
55 iBackScroll = -1;
56}
57
58void C4MessageBoard::Clear()
59{
60 Active = false;
61 LogBuffer.Clear();
62 LogBuffer.SetLBWidth(0);
63}
64
65void C4MessageBoard::ChangeMode(int inMode)
66{
67 if (iMode < 0 || iMode >= 3) return;
68
69 // prepare msg board
70 int iHeight = 0;
71 switch (inMode)
72 {
73 case 0: // one line, std
74
75 Config.Graphics.MsgBoard = 1;
76 iHeight = iLineHgt; // one line
77
78 // from mode 2?
79 if (iMode == 2)
80 {
81 // move to end of log
82 iBackScroll = -1;
83 // show nothing
84 Empty = true;
85 break;
86 }
87
88 // set msg pointer to end of log
89 iBackScroll = -1;
90 Fader = -1;
91 Empty = false;
92 Speed = 2;
93 ScreenFader = C4MSGB_MaxMsgFading * iLineHgt; // msgs faded out
94
95 break;
96
97 case 1: // >= 2 lines
98
99 iLines = (std::max)(a: iLines, b: 2);
100 Config.Graphics.MsgBoard = iLines;
101 // calc position; go to end
102 iBackScroll = -1;
103 // ok
104 iHeight = (iLines + 1) * iLineHgt;
105 Fader = 0;
106 iBackScroll = -1;
107 break;
108
109 case 2: // show nothing
110
111 Config.Graphics.MsgBoard = 0;
112 iHeight = 0;
113
114 break;
115 }
116 // set mode
117 iMode = inMode;
118 // recalc output facet
119 Output.X = 0;
120 Output.Y = Config.Graphics.ResY - iHeight;
121 Output.Hgt = iHeight;
122 LogBuffer.SetLBWidth(Output.Wdt);
123 // recalc viewports
124 Game.GraphicsSystem.RecalculateViewports();
125}
126
127void C4MessageBoard::Execute()
128{
129 if (!Active) return;
130
131 // Startup? draw only
132 if (Startup) { Draw(cgo&: Output); return; }
133
134 // which mode?
135 switch (iMode)
136 {
137 case 2: // show nothing
138
139 // TypeIn: Act as in mode 0
140 if (!Game.MessageInput.IsTypeIn())
141 {
142 ScreenFader = 100;
143 iBackScroll = -1;
144 break;
145 }
146
147 case 0: // one msg
148
149 // typein? fade in
150 if (Game.MessageInput.IsTypeIn())
151 ScreenFader = (std::max)(a: ScreenFader - 20, b: -100);
152
153 // no curr msg?
154 if (iBackScroll < 0)
155 {
156 // ok, it is empty
157 Empty = true;
158 // draw anyway
159 Draw(cgo&: Output);
160 if (!Game.MessageInput.IsTypeIn())
161 ScreenFader += 5;
162 return;
163 }
164 // got new msg?
165 if (Empty)
166 {
167 // start fade in
168 Fader = iLineHgt;
169 Delay = -1;
170 // now we have a msg
171 Empty = false;
172 }
173
174 // recalc fade/delay speed
175 Speed = (std::max)(a: 1, b: iBackScroll / 5);
176 // fade msg in?
177 if (Fader > 0)
178 Fader = (std::max)(a: Fader - Speed, b: 0);
179 // fade msg out?
180 if (Fader < 0)
181 Fader = (std::max)(a: Fader - Speed, b: -iLineHgt);
182 // hold curr msg? (delay)
183 if (Fader == 0)
184 {
185 // no delay set yet?
186 if (Delay == -1)
187 {
188 // set delay based on msg length
189 const char *szCurrMsg = LogBuffer.GetLine(iLineIndex: (std::min)(a: -iBackScroll, b: -1), ppFont: nullptr, pdwClr: nullptr, pNewParagraph: nullptr);
190 if (szCurrMsg) Delay = strlen(s: szCurrMsg); else Delay = 0;
191 }
192 // wait...
193 if (Delay > 0) Delay = (std::max)(a: Delay - Speed, b: 0);
194 // end of delay
195 if (Delay == 0)
196 {
197 Fader = (std::max)(a: -Speed, b: -iLineHgt); // start fade out
198 Delay = -1;
199 }
200 }
201
202 ScreenFader = (std::max)(a: ScreenFader - 20, b: -100);
203
204 // go to next msg? (last msg is completely faded out)
205 if (Fader == -iLineHgt)
206 {
207 // set cursor to next msg (or at end of log)
208 iBackScroll = (std::max)(a: iBackScroll - 1, b: -1);
209 // reset fade
210 Fader = 0;
211 }
212
213 break;
214
215 case 1: // > 2 msgs
216 break;
217 }
218
219 // Draw
220 Draw(cgo&: Output);
221}
222
223void C4MessageBoard::Init(C4Facet &cgo, bool fStartup)
224{
225 Active = true;
226 Output = cgo;
227 Startup = fStartup;
228 iLineHgt = Game.GraphicsResource.FontRegular.GetLineHeight();
229 LogBuffer.SetLBWidth(Output.Wdt);
230
231 if (!Startup)
232 {
233 // set cursor to end of log
234 iBackScroll = -1;
235 // load msgboard mode from config
236 iLines = Config.Graphics.MsgBoard;
237 if (iLines == 0) ChangeMode(inMode: 2);
238 if (iLines == 1) ChangeMode(inMode: 0);
239 if (iLines >= 2) ChangeMode(inMode: 1);
240 }
241}
242
243void C4MessageBoard::Draw(C4Facet &cgo)
244{
245 if (!Active || !Application.Active) return;
246
247 // Startup: draw Loader
248 if (Startup)
249 {
250 if (Game.GraphicsSystem.pLoaderScreen)
251 Game.GraphicsSystem.pLoaderScreen->Draw(cgo, iProgress: Game.InitProgress, pLog: &LogBuffer);
252 else
253 // loader not yet loaded: black BG
254 Application.DDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: 0, iY1: 0, iX2: cgo.Wdt, iY2: cgo.Hgt, dwClr: 0x00000000);
255 return;
256 }
257
258 // Game running: message fader
259 // Background
260 Application.DDraw->BlitSurfaceTile(sfcSurface: Game.GraphicsResource.fctBackground.Surface, sfcTarget: cgo.Surface, iToX: cgo.X, iToY: cgo.Y, iToWdt: Config.Graphics.ResX, iToHgt: cgo.Hgt, iOffsetX: -cgo.X, iOffsetY: -cgo.Y);
261
262 // draw messages
263 if (iMode != 2 || C4ChatInputDialog::IsShown())
264 {
265 // how many "extra" messages should be shown?
266 int iMsgFader = iMode == 1 ? 0 : C4MSGB_MaxMsgFading;
267 // check screenfader range
268 if (ScreenFader >= C4MSGB_MaxMsgFading * iLineHgt)
269 {
270 ScreenFader = C4MSGB_MaxMsgFading * iLineHgt;
271 iMsgFader = 0;
272 }
273 // show all msgs
274 int iLines = (iMode == 1) ? this->iLines : 2;
275 for (int iMsg = -iMsgFader - iLines; iMsg < 0; iMsg++)
276 {
277 // get message at pos
278 if (iMsg - iBackScroll >= 0) break;
279 const char *Message = LogBuffer.GetLine(iLineIndex: iMsg - iBackScroll, ppFont: nullptr, pdwClr: nullptr, pNewParagraph: nullptr);
280 if (!Message || !*Message) continue;
281 // calc target position (y)
282 int iMsgY = cgo.Y + (iMsg + (iLines - 1)) * iLineHgt + Fader;
283
284 // player message color?
285 C4Player *pPlr = GetMessagePlayer(szMessage: Message);
286
287 uint32_t dwColor;
288 if (pPlr)
289 dwColor = PlrClr2TxtClr(clr: pPlr->ColorDw) & 0xffffff;
290 else
291 dwColor = 0xffffff;
292 // fade out (msg fade)
293 uint32_t dwFade;
294 if (iMsgY < cgo.Y)
295 {
296 dwFade = (0xff - BoundBy(bval: (cgo.Y - iMsgY + (std::max)(a: ScreenFader, b: 0)) * 256 / (std::max)(a: iMsgFader, b: 1) / iLineHgt, lbound: 0, rbound: 0xff)) << 24;
297 Game.GraphicsSystem.OverwriteBg();
298 }
299 else
300 dwFade = 0xff000000;
301 dwColor |= dwFade;
302 // Draw
303 Application.DDraw->StringOut(szText: Message, rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X, iTy: iMsgY, dwFCol: dwColor);
304 }
305 }
306}
307
308void C4MessageBoard::EnsureLastMessage()
309{
310 // Ingore if startup or typein
311 if (!Active || Startup) return;
312 // not active: do nothing
313 if (iMode == 2) return;
314 // "console" mode: just set BackScroll to 0 and draw
315 if (iMode == 1) { iBackScroll = 0; Game.GraphicsSystem.Execute(); return; }
316 // scroll mode: scroll until end of log
317 for (int i = 0; i < 100; i++)
318 {
319 Game.GraphicsSystem.Execute();
320 Execute();
321 if (iBackScroll < 0) break;
322 Delay = 0;
323 }
324}
325
326void C4MessageBoard::AddLog(const char *szMessage)
327{
328 // Not active
329 if (!Active) return;
330 // safety
331 if (!szMessage || !*szMessage) return;
332 // make sure new message will be drawn
333 ++iBackScroll;
334
335 StdStrBuf text;
336
337 if (Config.General.ShowLogTimestamps)
338 {
339 text.Append(pnData: GetCurrentTimeStamp());
340 text.AppendChar(cChar: ' ');
341 }
342 text.Append(pnData: szMessage);
343
344 // register message in standard messageboard font
345 LogBuffer.AppendLines(szLine: text.getData(), pFont: &Game.GraphicsResource.FontRegular, dwClr: 0, pFirstLineFont: nullptr);
346}
347
348void C4MessageBoard::ClearLog()
349{
350 LogBuffer.Clear();
351}
352
353void C4MessageBoard::LogNotify()
354{
355 // Not active
356 if (!Active) return;
357 // do not show startup board if GUI is active
358 if (Game.pGUI && Game.pGUI->IsActive()) return;
359 // Reset
360 iBackScroll = 0;
361 // Draw
362 Draw(cgo&: Output);
363 // startup: Draw message board only and do page flip
364 if (Startup) Application.DDraw->PageFlip();
365}
366
367C4Player *C4MessageBoard::GetMessagePlayer(const char *szMessage)
368{
369 std::string message{szMessage};
370 // Scan message text for heading player name
371 if (SEqual2(szStr1: szMessage, szStr2: "* "))
372 {
373 message = message.substr(pos: 2);
374 return Game.Players.GetByName(szName: message.substr(pos: 0, n: message.find(c: ' ')).c_str());
375 }
376 if (SCharCount(cTarget: ':', szInStr: szMessage))
377 {
378 return Game.Players.GetByName(szName: message.substr(pos: 0, n: message.find(c: ':')).c_str());
379 }
380 return nullptr;
381}
382
383bool C4MessageBoard::ControlScrollUp()
384{
385 if (!Active) return false;
386 Delay = -1; Fader = 0; Empty = false;
387 iBackScroll++;
388 return true;
389}
390
391bool C4MessageBoard::ControlScrollDown()
392{
393 if (!Active) return false;
394 Delay = -1; Fader = 0; Empty = false;
395 if (iBackScroll > -1) iBackScroll--;
396 return true;
397}
398