| 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 | |
| 29 | C4LogBuffer::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 | |
| 46 | C4LogBuffer::~C4LogBuffer() |
| 47 | { |
| 48 | // free buffers |
| 49 | delete[] pLineDataBuf; |
| 50 | delete[] szBuf; |
| 51 | // free indent |
| 52 | delete[] szIndent; |
| 53 | } |
| 54 | |
| 55 | void 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 | |
| 65 | void 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 | |
| 75 | void 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 | |
| 96 | void 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 | |
| 174 | void 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 | |
| 259 | const 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 | |
| 287 | void C4LogBuffer::Clear() |
| 288 | { |
| 289 | // clear buffer usage |
| 290 | iFirstLinePos = iAfterLastLinePos = iLineCount = iNextLineDataPos = iLineDataPos = 0; |
| 291 | } |
| 292 | |
| 293 | void C4LogBuffer::SetLBWidth(int iToWidth) |
| 294 | { |
| 295 | iLineBreakWidth = iToWidth; |
| 296 | } |
| 297 | |