Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for correctly rendering watch streak messages #890

Merged
merged 1 commit into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions TwitchDownloaderCore/ChatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,11 +614,11 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
var finalBitmapInfo = finalBitmap.Info;
using (SKCanvas finalCanvas = new SKCanvas(finalBitmap))
{
if (highlightType is HighlightType.PayingForward or HighlightType.ChannelPointHighlight)
if (highlightType is HighlightType.PayingForward or HighlightType.ChannelPointHighlight or HighlightType.WatchStreak)
{
var accentColor = highlightType is HighlightType.PayingForward
? new SKColor(0x26, 0x26, 0x2C, 0xFF) // #26262C (RRGGBB)
: new SKColor(0x80, 0x80, 0x8C, 0xFF); // #80808C (RRGGBB)
? new SKColor(0xFF26262C) // AARRGGBB
: new SKColor(0xFF80808C); // AARRGGBB

using var paint = new SKPaint { Color = accentColor };
finalCanvas.DrawRect(renderOptions.SidePadding, 0, renderOptions.AccentStrokeWidth, finalBitmapInfo.Height, paint);
Expand All @@ -630,7 +630,7 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
(renderOptions.AlternateMessageBackgrounds && renderOptions.AlternateBackgroundColor.Alpha < OPAQUE_THRESHOLD)))
{
// Draw the highlight background only if the message background is opaque enough
var backgroundColor = new SKColor(0x6B, 0x6B, 0x6E, 0x1A); // #1A6B6B6E (AARRGGBB)
var backgroundColor = new SKColor(0x1A6B6B6E); // AARRGGBB
using var backgroundPaint = new SKPaint { Color = backgroundColor };
finalCanvas.DrawRect(renderOptions.SidePadding, 0, finalBitmapInfo.Width - renderOptions.SidePadding * 2, finalBitmapInfo.Height, backgroundPaint);
}
Expand Down Expand Up @@ -706,6 +706,9 @@ private void DrawAccentedMessage(Comment comment, List<(SKImageInfo info, SKBitm
case HighlightType.BitBadgeTierNotification:
DrawBitsBadgeTierMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
break;
case HighlightType.WatchStreak:
DrawWatchStreakMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
break;
case HighlightType.GiftedMany:
case HighlightType.GiftedSingle:
case HighlightType.GiftedAnonymous:
Expand Down Expand Up @@ -802,6 +805,45 @@ private void DrawBitsBadgeTierMessage(Comment comment, List<(SKImageInfo info, S
DrawMessage(comment, sectionImages, emotePositionList, false, ref drawPos, defaultPos);
}

private void DrawWatchStreakMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
{
using SKCanvas canvas = new(sectionImages.Last().bitmap);
canvas.DrawImage(highlightIcon, iconPoint.X, iconPoint.Y);

Point customMessagePos = drawPos;
drawPos.X += highlightIcon.Width + renderOptions.WordSpacing;
defaultPos.X = drawPos.X;

DrawUsername(comment, sectionImages, ref drawPos, defaultPos, false, Purple);
AddImageSection(sectionImages, ref drawPos, defaultPos);

// Remove the commenter's name from the watch streak message
comment.message.body = comment.message.body[(comment.commenter.display_name.Length + 1)..];
if (comment.message.fragments[0].text.Equals(comment.commenter.display_name, StringComparison.OrdinalIgnoreCase))
{
// This is necessary for sub messages. We'll keep it around just in case.
comment.message.fragments.RemoveAt(0);
}
else
{
comment.message.fragments[0].text = comment.message.fragments[0].text[(comment.commenter.display_name.Length + 1)..];
}

var (streakMessage, customMessage) = HighlightIcons.SplitWatchStreakComment(comment);
DrawMessage(streakMessage, sectionImages, emotePositionList, false, ref drawPos, defaultPos);

// Return if there is no custom message to draw
if (customMessage is null)
{
return;
}

AddImageSection(sectionImages, ref drawPos, defaultPos);
drawPos = customMessagePos;
defaultPos = customMessagePos;
DrawNonAccentedMessage(customMessage, sectionImages, emotePositionList, false, ref drawPos, ref defaultPos);
}

private void DrawGiftMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
{
using SKCanvas canvas = new(sectionImages.Last().bitmap);
Expand Down
66 changes: 56 additions & 10 deletions TwitchDownloaderCore/Tools/HighlightIcons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum HighlightType
ChannelPointHighlight,
Raid,
BitBadgeTierNotification,
WatchStreak,
Unknown
}

Expand All @@ -32,16 +33,19 @@ public sealed class HighlightIcons : IDisposable
private const string GIFTED_MANY_ICON_URL = "https://static-cdn.jtvnw.net/subs-image-assets/gift-illus.png";
private const string GIFTED_ANONYMOUS_ICON_SVG = "m 54.571425,64.514958 a 4.3531428,4.2396967 0 0 1 -1.273998,-0.86096 l -1.203426,-1.172067 a 7.0051428,6.822584 0 0 0 -9.90229,0 c -3.417139,3.328092 -8.962569,3.328092 -12.383427,0 l -0.159707,-0.155553 a 7.1871427,6.9998405 0 0 0 -9.854005,-0.28216 l -1.894286,1.635103 a 4.9362858,4.8076423 0 0 1 -3.276,1.215474 H 10 V 32.337399 a 26.000001,25.322423 0 0 1 52,0 v 32.557396 h -5.627146 c -0.627714,0 -1.240569,-0.133847 -1.801429,-0.379837 z M 35.999996,14.249955 A 18.571428,18.087444 0 0 0 17.428572,32.337399 v 22.515245 a 14.619428,14.238435 0 0 1 17.471998,2.358609 l 0.163448,0.155554 c 0.516285,0.50645 1.355715,0.50645 1.875712,0 a 14.437428,14.061179 0 0 1 17.631712,-2.11623 V 32.337399 A 18.571428,18.087444 0 0 0 35.999996,14.249955 Z M 24.857142,35.954887 a 3.7142855,3.6174889 0 1 1 7.42857,0 3.7142855,3.6174889 0 0 1 -7.42857,0 z m 18.571432,-3.617488 a 3.7142859,3.6174892 0 1 0 0,7.234978 3.7142859,3.6174892 0 0 0 0,-7.234978 z";
private const string BIT_BADGE_TIER_NOTIFICATION_ICON_SVG = "M 14.242705,42.37453 36,11.292679 57.757295,42.37453 36,61.023641 Z M 22.566425,41.323963 36,22.13092 49.433577,41.317747 46.79162,43.580506 36,39.266345 25.205273,43.586723 22.566425,41.320854 Z";
private const string WATCH_STREAK_ICON_SVG = "M 38.84325,21.169078 33.156748,14.060989 21.215093,27.992844 a 21.267516,21.267402 0 0 0 -5.11785,13.846557 c 0,9.752298 7.961102,17.713358 17.713453,17.713358 H 38.50206 A 17.400696,17.400602 0 0 0 55.902755,42.152157 c 0,-5.288419 -1.848114,-10.406242 -5.231581,-14.500501 L 41.686501,16.904225 Z m -13.306415,10.519973 7.619913,-9.098354 5.686502,7.108089 2.843251,-4.264854 4.606066,5.885497 a 16.945776,16.945684 0 0 1 3.923686,10.832728 c 0,5.91393 -4.407039,10.804296 -10.121973,11.600401 1.02357,-1.336321 1.592221,-2.985397 1.592221,-4.719771 0,-1.478483 -0.511786,-2.900101 -1.421626,-4.065827 l -4.264877,-5.316851 -4.264876,5.316851 c -0.90984,1.137294 -1.421625,2.587344 -1.421625,4.065827 0,1.705941 0.56865,3.355018 1.535355,4.662906 A 12.026952,12.026887 0 0 1 21.783744,41.839401 c 0,-3.72464 1.336328,-7.335548 3.753091,-10.15035 z";

private static readonly Regex SubMessageRegex = new(@"^(subscribed (?:with Prime|at Tier \d)\. They've subscribed for \d{1,3} months(?:, currently on a \d{1,3} month streak)?! )(.+)$", RegexOptions.Compiled);
private static readonly Regex GiftAnonymousRegex = new(@"^An anonymous user (?:gifted a|is gifting \d{1,4}) Tier \d", RegexOptions.Compiled);
private static readonly Regex WatchStreakRegex = new(@"^(watched \d+ consecutive streams this month and sparked a watch streak! )(.*)$", RegexOptions.Compiled);

private SKImage _subscribedTierIcon;
private SKImage _subscribedPrimeIcon;
private SKImage _giftSingleIcon;
private SKImage _giftManyIcon;
private SKImage _giftAnonymousIcon;
private SKImage _bitBadgeTierNotificationIcon;
private SKImage _watchStreakIcon;

private readonly string _cachePath;
private readonly SKColor _purple;
Expand Down Expand Up @@ -86,6 +90,9 @@ public static HighlightType GetHighlightType(Comment comment)
if (bodyWithoutName.StartsWith(" is paying forward the Gift they got from"))
return HighlightType.PayingForward;

if (bodyWithoutName.Contains(" consecutive streams this month and sparked a watch streak!", StringComparison.Ordinal))
return HighlightType.WatchStreak;

if (bodyWithoutName.StartsWith(" converted from a"))
{
// TODO: use bodyWithoutName when .NET 7
Expand Down Expand Up @@ -134,6 +141,7 @@ public SKImage GetHighlightIcon(HighlightType highlightType, SKColor textColor,
HighlightType.GiftedMany => _giftManyIcon ??= GenerateGiftedManyIcon(fontSize, _cachePath, _offline),
HighlightType.GiftedAnonymous => _giftAnonymousIcon ??= GenerateSvgIcon(GIFTED_ANONYMOUS_ICON_SVG, textColor, fontSize),
HighlightType.BitBadgeTierNotification => _bitBadgeTierNotificationIcon ??= GenerateSvgIcon(BIT_BADGE_TIER_NOTIFICATION_ICON_SVG, textColor, fontSize),
HighlightType.WatchStreak => _watchStreakIcon ??= GenerateSvgIcon(WATCH_STREAK_ICON_SVG, textColor, fontSize),
_ => null
};
}
Expand Down Expand Up @@ -196,13 +204,16 @@ private static SKImage GenerateSvgIcon(string iconSvgString, SKColor iconColor,
/// </returns>
public static (Comment subMessage, Comment customMessage) SplitSubComment(Comment comment)
{
var (subMessage, customMessage) = SplitSubMessage(comment.message.body);
// Return the original comment + null if there is no custom sub message
if (customMessage is null)
var subMessageMatch = SubMessageRegex.Match(comment.message.body);
if (!subMessageMatch.Success)
{
// Return the original comment + null if there is no custom sub message
return (comment, null);
}

var subMessage = subMessageMatch.Groups[1].Value;
var customMessage = subMessageMatch.Groups[2].Value;

// If we don't clone then both new comments reference the original commenter object, message object, fragment list, etc.
var subMessageComment = comment.Clone();
subMessageComment.message.body = subMessage;
Expand Down Expand Up @@ -232,18 +243,53 @@ public static (Comment subMessage, Comment customMessage) SplitSubComment(Commen
return (subMessageComment, customMessageComment);
}

/// <returns>The split re-sub details and user's custom re-sub message if there is one, else the re-sub details and null</returns>
public static (string subMessage, string customMessage) SplitSubMessage(string commentMessage)
/// <summary>
/// Splits a comment into 2 comments based on the start index of a custom re-sub message
/// </summary>
/// <returns>
/// 2 clones of <paramref name="comment"/> whose <see cref="Message.body"/> and <see cref="Message.fragments"/> contain the split re-sub details and
/// the user's custom re-sub message if there is one, else the original <paramref name="comment"/> and null
/// </returns>
public static (Comment subMessage, Comment customMessage) SplitWatchStreakComment(Comment comment)
{
var subMessageMatch = SubMessageRegex.Match(commentMessage);
if (!subMessageMatch.Success)
var watchStreakMatch = WatchStreakRegex.Match(comment.message.body);
if (!watchStreakMatch.Success)
{
return (commentMessage, null);
// Return the original comment + null if there is no custom watch streak message
return (comment, null);
}

return (subMessageMatch.Groups[1].Value, subMessageMatch.Groups[2].Value);
}
var streakMessage = watchStreakMatch.Groups[1].Value;
var customMessage = watchStreakMatch.Groups[2].Value;

// If we don't clone then both new comments reference the original commenter object, message object, fragment list, etc.
var streakMessageComment = comment.Clone();
streakMessageComment.message.body = streakMessage;
streakMessageComment.message.fragments[0].text = streakMessage;
var customMessageComment = comment.Clone();
customMessageComment.message.body = customMessage;

// If only one fragment then we are done
if (comment.message.fragments.Count == 1)
{
customMessageComment.message.fragments[0].text = customMessage;
return (streakMessageComment, customMessageComment);
}

streakMessageComment.message.fragments.RemoveRange(1, comment.message.fragments.Count - 1);
streakMessageComment.message.emoticons.Clear();

// Check to see if there is a custom message before the next fragment
// i.e. Foobar watched 3 consecutive streams this month and sparked a watch streak! Hey PogChamp
if (!customMessage.StartsWith(comment.message.fragments[1].text)) // If yes
{
customMessageComment.message.fragments[0].text = customMessage[..(customMessage.IndexOf(comment.message.fragments[1].text, StringComparison.Ordinal) - 1)];
return (streakMessageComment, customMessageComment);
}

customMessageComment.message.fragments.RemoveAt(0);
return (streakMessageComment, customMessageComment);
}
#region ImplementIDisposable

public void Dispose()
Expand Down
Loading