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 buffer holding a log history
19
20#include "C4LogBuf.h"
21
22#include "Standard.h"
23#include "StdFont.h"
24
25#include <algorithm>
26#include <cassert>
27#include <cstring>
28
29C4LogBuffer::C4LogBuffer(size_t iSize, size_t iMaxLines, int iLBWidth, const char *szIndentChars, bool fDynamicGrow, bool fMarkup)
30 : iBufSize(iSize), iFirstLinePos(0), iAfterLastLinePos(0), iLineDataPos(0),
31 iNextLineDataPos(0), iMaxLineCount(iMaxLines), iLineCount(0), iLineBreakWidth(iLBWidth), fDynamicGrow(fDynamicGrow), fMarkup(fMarkup)
32{
33 // copy indent
34 if (szIndentChars && *szIndentChars)
35 {
36 szIndent = new char[strlen(s: szIndentChars) + 1];
37 strcpy(dest: szIndent, src: szIndentChars);
38 }
39 else szIndent = nullptr;
40 // create buffers, if buffer size is given. Otherwise, create/grow them dynamically
41 if (iBufSize) szBuf = new char[iBufSize]; else szBuf = nullptr;
42 if (iMaxLineCount) pLineDataBuf = new LineData[iMaxLineCount]; else pLineDataBuf = nullptr;
43 assert(fDynamicGrow || (iBufSize && iMaxLineCount));
44}
45
46C4LogBuffer::~C4LogBuffer()
47{
48 // free buffers
49 delete[] pLineDataBuf;
50 delete[] szBuf;
51 // free indent
52 delete[] szIndent;
53}
54
55void C4LogBuffer::GrowLineCountBuffer(size_t iGrowBy)
56{
57 assert(fDynamicGrow);
58 if (!iGrowBy) return;
59 LineData *pNewBuf = new LineData[iMaxLineCount += iGrowBy];
60 if (iLineCount) memcpy(dest: pNewBuf, src: pLineDataBuf, n: sizeof(LineData) * iLineCount);
61 delete[] pLineDataBuf;
62 pLineDataBuf = pNewBuf;
63}
64
65void C4LogBuffer::GrowTextBuffer(size_t iGrowBy)
66{
67 assert(fDynamicGrow);
68 if (!iGrowBy) return;
69 char *pNewBuf = new char[iBufSize += iGrowBy];
70 if (iAfterLastLinePos) memcpy(dest: pNewBuf, src: szBuf, n: sizeof(char) * iAfterLastLinePos);
71 delete[] szBuf;
72 szBuf = pNewBuf;
73}
74
75void C4LogBuffer::DiscardFirstLine()
76{
77 // any line to discard? - this is guaranteed (private call)
78 assert(iLineCount && szBuf && !fDynamicGrow);
79 // dec line count
80 --iLineCount;
81 // advance first line pos until delimeter char is reached
82 while (szBuf[iFirstLinePos]) ++iFirstLinePos;
83 // skip delimeter
84 ++iFirstLinePos;
85 // check if end of used buffer is reached (by size or double delimeter)
86 if (iFirstLinePos == iBufSize || !szBuf[iFirstLinePos] || !iLineCount)
87 {
88 // end of buffer reached: wrap to front
89 iFirstLinePos = 0;
90 }
91 // discard line data
92 ++iLineDataPos;
93 if (!iLineCount) iLineDataPos = iNextLineDataPos = 0;
94}
95
96void C4LogBuffer::AppendSingleLine(const char *szLine, size_t iLineLength, const char *szIndent, CStdFont *pFont, uint32_t dwClr, bool fNewPar)
97{
98 // security: do not append empty line
99 if (!szLine || !iLineLength || !*szLine) return;
100 // discard first line or grow buffer if data buffer is full
101 if (iLineCount == iMaxLineCount)
102 if (fDynamicGrow)
103 GrowLineCountBuffer(iGrowBy: 4 + iMaxLineCount / 2);
104 else
105 DiscardFirstLine();
106 // include trailing zero-character
107 if (szLine[iLineLength] == '\0') ++iLineLength;
108 // include indent
109 if (szIndent) iLineLength += strlen(s: szIndent);
110 // but do not add a message that is longer than the buffer (shouldn't happen anyway)
111 if (iLineLength > iBufSize && !fDynamicGrow)
112 {
113 // cut from beginning then
114 szLine += iLineLength - iBufSize;
115 iLineLength = iBufSize;
116 }
117 // check if the rest of the buffer is sufficient
118 if (iAfterLastLinePos + iLineLength > iBufSize)
119 {
120 if (fDynamicGrow)
121 {
122 // insufficient buffer in grow mode: grow text buffer
123 GrowTextBuffer(iGrowBy: (std::max)(a: iLineLength, b: iBufSize / 2));
124 }
125 else
126 {
127 // insufficient buffer in non-grow mode: wrap to beginning
128 // discard any messages in rest of buffer
129 // if there are no messages, iFirstLinePos is always zero
130 // and iAfterLastLinePos cannot be zero here
131 while (iFirstLinePos >= iAfterLastLinePos) DiscardFirstLine();
132 // add delimeter to mark end of used buffer
133 // if the buffer is exactly full by the last line, no delimeter is needed
134 if (iAfterLastLinePos < iBufSize) szBuf[iAfterLastLinePos] = 0;
135 // wrap insertion pos to beginning
136 iAfterLastLinePos = 0;
137 }
138 }
139 // discard any messages within insertion range of new message
140 if (!fDynamicGrow)
141 while (iLineCount && Inside<size_t>(ival: iFirstLinePos, lbound: iAfterLastLinePos, rbound: iAfterLastLinePos + iLineLength - 1))
142 DiscardFirstLine();
143 // copy indent
144 size_t iIndentLen = 0;
145 if (szIndent)
146 {
147 iIndentLen = strlen(s: szIndent);
148 memcpy(dest: szBuf + iAfterLastLinePos, src: szIndent, n: iIndentLen);
149 }
150 // copy message
151 if (iLineLength - iIndentLen > 1)
152 memcpy(dest: szBuf + iAfterLastLinePos + iIndentLen, src: szLine, n: iLineLength - iIndentLen - 1);
153 // add delimeter
154 iAfterLastLinePos += iLineLength;
155 szBuf[iAfterLastLinePos - 1] = 0;
156 // no need to add any double delimeters, because this is currently the end of the message list
157 // also no need to check for end of buffer here, because that will be done when the next message is inserted
158 // add line data
159 LineData &rData = pLineDataBuf[iNextLineDataPos];
160 rData.pFont = pFont;
161 rData.dwClr = dwClr;
162 rData.fNewParagraph = fNewPar;
163 // new message successfully added; count it
164 ++iLineCount;
165 if (++iNextLineDataPos == iMaxLineCount)
166 {
167 if (fDynamicGrow)
168 GrowLineCountBuffer(iGrowBy: 4 + iMaxLineCount / 2);
169 else
170 iNextLineDataPos = 0;
171 }
172}
173
174void C4LogBuffer::AppendLines(const char *szLine, CStdFont *pFont, uint32_t dwClr, CStdFont *pFirstLineFont)
175{
176 char LineBreakChars[] = { 0x0D, 0x0A, '|' };
177 int iLineBreakCharCount = 2 + fMarkup;
178 // safety
179 if (!szLine) return;
180 // split '|'/CR/LF-separations first, if there are any
181 bool fAnyLineBreakChar = false;
182 for (int i = 0; i < iLineBreakCharCount; ++i)
183 if (strchr(s: szLine, c: LineBreakChars[i]))
184 {
185 fAnyLineBreakChar = true;
186 break;
187 }
188 if (fAnyLineBreakChar)
189 {
190 char *szBuf = new char[strlen(s: szLine) + 1];
191 char *szBufPos, *szPos2 = szBuf, *szBufFind;
192 strcpy(dest: szBuf, src: szLine);
193 while (szBufPos = szPos2)
194 {
195 // find first occurance of any line break char
196 szPos2 = nullptr;
197 for (int i = 0; i < iLineBreakCharCount; ++i)
198 if (szBufFind = strchr(s: szBufPos, c: LineBreakChars[i]))
199 if (!szPos2 || szBufFind < szPos2)
200 szPos2 = szBufFind;
201 // split string at linebreak char
202 if (szPos2) *szPos2++ = '\0';
203 // output current line if not empty
204 if (!*szBufPos) continue;
205 // first line in caption font
206 if (pFirstLineFont)
207 {
208 AppendLines(szLine: szBufPos, pFont: pFirstLineFont, dwClr);
209 pFirstLineFont = nullptr;
210 }
211 else
212 AppendLines(szLine: szBufPos, pFont, dwClr);
213 }
214 delete[] szBuf;
215 return;
216 }
217 // no line breaks desired: Output all in one line
218 if (!iLineBreakWidth || !pFont)
219 {
220 AppendSingleLine(szLine, iLineLength: strlen(s: szLine), szIndent: nullptr, pFont, dwClr, fNewPar: true);
221 }
222 else
223 {
224 int iLineIndex = 0;
225 // get line width of this line
226 int iBreakWdt = iLineBreakWidth;
227 int32_t iIndentWdt = 0;
228 if (szIndent)
229 {
230 int32_t Q;
231 pFont->GetTextExtent(szText: szIndent, rsx&: iIndentWdt, rsy&: Q, fCheckMarkup: true);
232 }
233 StdStrBuf broken;
234
235 // first without indentation
236 pFont->BreakMessage(szMsg: szLine, iWdt: iBreakWdt, pOut: &broken, fCheckMarkup: true, fZoom: 1.0f, maxLines: 1);
237 const char *szBroken = broken.getData(), *breakPos;
238 if (!(breakPos = strchr(s: szBroken, c: '\n'))) breakPos = szBroken + SLen(sptr: szBroken);
239 else ++breakPos;
240
241 AppendSingleLine(szLine: szBroken, iLineLength: breakPos - szBroken, szIndent: nullptr, pFont, dwClr, fNewPar: true);
242 ++iLineIndex;
243
244 // then with indentation
245 StdStrBuf rest;
246 pFont->BreakMessage(szMsg: breakPos, iWdt: iBreakWdt - iIndentWdt, pOut: &rest, fCheckMarkup: true);
247 szBroken = rest.getData();
248 while (*szBroken)
249 {
250 if (!(breakPos = strchr(s: szBroken, c: '\n'))) breakPos = szBroken + SLen(sptr: szBroken);
251 else ++breakPos;
252 AppendSingleLine(szLine: szBroken, iLineLength: breakPos - szBroken, szIndent, pFont, dwClr, fNewPar: false);
253 szBroken = breakPos;
254 ++iLineIndex;
255 }
256 }
257}
258
259const char *C4LogBuffer::GetLine(int iLineIndex, CStdFont **ppFont, uint32_t *pdwClr, bool *pfNewPar) const
260{
261 // evaluate negative indices
262 if (iLineIndex < 0)
263 {
264 iLineIndex += iLineCount;
265 if (iLineIndex < 0) return nullptr;
266 }
267 // range check
268 if (iLineIndex >= iLineCount) return nullptr;
269 // assign data
270 LineData &rData = pLineDataBuf[(iLineDataPos + iLineIndex) % iMaxLineCount];
271 if (ppFont) *ppFont = rData.pFont;
272 if (pdwClr) *pdwClr = rData.dwClr;
273 if (pfNewPar) *pfNewPar = rData.fNewParagraph;
274 // advance in lines until desired line is found
275 char *szResult = szBuf + iFirstLinePos;
276 while (iLineIndex--)
277 {
278 // skip this line
279 while (*szResult++);
280 // double delimeter or end of buffer: reset searching to front of buffer
281 if (szResult == (szBuf + iBufSize) || !*szResult) szResult = szBuf;
282 }
283 // return found buffer pos
284 return szResult;
285}
286
287void C4LogBuffer::Clear()
288{
289 // clear buffer usage
290 iFirstLinePos = iAfterLastLinePos = iLineCount = iNextLineDataPos = iLineDataPos = 0;
291}
292
293void C4LogBuffer::SetLBWidth(int iToWidth)
294{
295 iLineBreakWidth = iToWidth;
296}
297