| 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 | |
| 29 | const int32_t TextMsgDelayFactor = 2; // frames per char message display time |
| 30 | |
| 31 | C4GameMessage::C4GameMessage() : pFrameDeco(nullptr) {} |
| 32 | |
| 33 | C4GameMessage::~C4GameMessage() |
| 34 | { |
| 35 | delete pFrameDeco; |
| 36 | } |
| 37 | |
| 38 | 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, 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 | |
| 73 | void 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 | |
| 86 | bool C4GameMessage::Execute() |
| 87 | { |
| 88 | // Delay / removal |
| 89 | if (Delay > 0) Delay--; |
| 90 | if (Delay == 0) return false; |
| 91 | // Done |
| 92 | return true; |
| 93 | } |
| 94 | |
| 95 | int32_t DrawMessageOffset = -35; // For finding the optimum place to draw startup info & player messages... |
| 96 | int32_t PortraitWidth = 64; |
| 97 | int32_t PortraitIndent = 10; |
| 98 | |
| 99 | void 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 | |
| 233 | void 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 | |
| 246 | C4GameMessageList::C4GameMessageList() |
| 247 | { |
| 248 | Clear(); |
| 249 | } |
| 250 | |
| 251 | C4GameMessageList::~C4GameMessageList() |
| 252 | { |
| 253 | Clear(); |
| 254 | } |
| 255 | |
| 256 | void 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 | |
| 264 | void C4GameMessageList::Clear() |
| 265 | { |
| 266 | Messages.clear(); |
| 267 | } |
| 268 | |
| 269 | void 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 | |
| 277 | bool 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 | |
| 285 | 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, 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 | |
| 290 | 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, 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 | |
| 315 | bool 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 | |
| 332 | void 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 | |
| 340 | void C4GameMessageList::UpdateDef(C4ID idUpdDef) |
| 341 | { |
| 342 | for (const auto &it : Messages) |
| 343 | { |
| 344 | it->UpdateDef(idUpdDef); |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | void C4GameMessageList::Draw(C4FacetEx &cgo, int32_t iPlayer) |
| 349 | { |
| 350 | for (const auto &it : Messages) |
| 351 | { |
| 352 | it->Draw(cgo, iPlayer); |
| 353 | } |
| 354 | } |
| 355 | |