Beispiel #1
0
        private void AppendLabel(string Label, string Value, StringBuilder Markdown)
        {
            bool First = true;

            Markdown.Append("| ");
            Markdown.Append(MarkdownDocument.Encode(Label));
            Markdown.Append(" | ");

            if (Value.Length > 2 && Value.StartsWith(":") && Value.EndsWith(":") &&
                EmojiUtilities.TryGetEmoji(Value.Substring(1, Value.Length - 2), out EmojiInfo _))
            {
                Markdown.Append(Value);
            }
            else
            {
                foreach (string Row in Value.Trim().Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'))
                {
                    if (First)
                    {
                        First = false;
                    }
                    else
                    {
                        Markdown.Append("<br/>");
                    }

                    Markdown.Append(MarkdownDocument.Encode(Row));
                }
            }

            Markdown.AppendLine(" |");
        }
Beispiel #2
0
        public async Task JumboAsync(
            [Summary("The emoji to jumbofy.")]
            string emoji)
        {
            var emojiUrl = EmojiUtilities.GetUrl(emoji);

            try
            {
                var client = HttpClientFactory.CreateClient();
                var req    = await client.GetStreamAsync(emojiUrl);

                await Context.Channel.SendFileAsync(req, Path.GetFileName(emojiUrl), Context.User.Mention);

                try
                {
                    await Context.Message.DeleteAsync();
                }
                catch (HttpRequestException ex)
                {
                    Log.Warning(ex, "Couldn't delete message after jumbofying.");
                }
            }
            catch (HttpRequestException ex)
            {
                Log.Warning(ex, "Failed jumbofying emoji");
                await ReplyAsync($"Sorry {Context.User.Mention}, I don't recognize that emoji.");
            }
        }
Beispiel #3
0
        private static void ProcessingThread(byte[] gifData, GifInfo frameInfo)
        {
            var gifImage   = EmojiUtilities.byteArrayToImage(gifData);
            var dimension  = new System.Drawing.Imaging.FrameDimension(gifImage.FrameDimensionsList[0]);
            int frameCount = gifImage.GetFrameCount(dimension);

            frameInfo.frameCount  = frameCount;
            frameInfo.initialized = true;

            int index           = 0;
            int firstDelayValue = -1;

            for (int i = 0; i < frameCount; i++)
            {
                gifImage.SelectActiveFrame(dimension, i);
                System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(gifImage.Width, gifImage.Height);
                System.Drawing.Graphics.FromImage(bitmap).DrawImage(gifImage, System.Drawing.Point.Empty);
                LockBitmap frame = new LockBitmap(bitmap);
                frame.LockBits();
                FrameInfo currentFrame = new FrameInfo(bitmap.Width, bitmap.Height);

                if (currentFrame.colors == null)
                {
                    currentFrame.colors = new Color32[frame.Height * frame.Width];
                }
                for (int x = 0; x < frame.Width; x++)
                {
                    for (int y = 0; y < frame.Height; y++)
                    {
                        System.Drawing.Color sourceColor = frame.GetPixel(x, y);
                        currentFrame.colors[(frame.Height - y - 1) * frame.Width + x] = new Color32(sourceColor.R, sourceColor.G, sourceColor.B, sourceColor.A);
                    }
                }

                int delayPropertyValue = BitConverter.ToInt32(gifImage.GetPropertyItem(20736).Value, index);
                if (firstDelayValue == -1)
                {
                    firstDelayValue = delayPropertyValue;
                }

                if (delayPropertyValue != firstDelayValue)
                {
                    frameInfo.isDelayConsistent = false;
                }

                currentFrame.delay = delayPropertyValue * 10;
                frameInfo.frames.Add(currentFrame);
                index += 4;

                Thread.Sleep(Globals.IsAtMainMenu ? 0 : 10);
            }
        }
Beispiel #4
0
        private static void ProcessingThread(byte[] gifData, GifInfo frameInfo)
        {
            var gifImage   = EmojiUtilities.byteArrayToImage(gifData);
            var dimension  = new System.Drawing.Imaging.FrameDimension(gifImage.FrameDimensionsList[0]);
            int frameCount = gifImage.GetFrameCount(dimension);

            frameInfo.frameCount  = frameCount;
            frameInfo.dimension   = dimension;
            frameInfo.initialized = true;

            int index = 0;

            for (int i = 0; i < frameCount; i++)
            {
                gifImage.SelectActiveFrame(dimension, i);
                System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(gifImage.Width, gifImage.Height);
                System.Drawing.Graphics.FromImage(bitmap).DrawImage(gifImage, System.Drawing.Point.Empty);
                LockBitmap frame = new LockBitmap(bitmap);

                frame.LockBits();
                FrameInfo currentFrame = new FrameInfo(bitmap.Width, bitmap.Height);

                if (currentFrame.colors == null)
                {
                    currentFrame.colors = new Color32[frame.Height * frame.Width];
                }

                for (int x = 0; x < frame.Width; x++)
                {
                    for (int y = 0; y < frame.Height; y++)
                    {
                        System.Drawing.Color sourceColor = frame.GetPixel(x, y);
                        currentFrame.colors[(frame.Height - y - 1) * frame.Width + x] = new Color32(sourceColor.R, sourceColor.G, sourceColor.B, sourceColor.A);
                    }
                }

                int delayPropertyValue = BitConverter.ToInt32(gifImage.GetPropertyItem(20736).Value, index);
                // If the delay property is 0, assume that it's a 10fps emote
                if (delayPropertyValue == 0)
                {
                    delayPropertyValue = 10;
                }

                currentFrame.delay = (float)delayPropertyValue / 100.0f;
                frameInfo.frames.Add(currentFrame);
                index += 4;

                Thread.Sleep(0);
            }
        }
Beispiel #5
0
        public static void OverlayImage(CustomText currentMessage, ImageInfo imageInfo)
        {
            CachedSpriteData cachedTextureData = imageInfo.cachedSprite;

            // If cachedTextureData is null, the emote will be overlayed at a later time once it's finished being cached
            if (cachedTextureData == null || (cachedTextureData.sprite == null && cachedTextureData.animInfo == null))
            {
                return;
            }

            bool animatedEmote      = cachedTextureData.animInfo != null;
            var  messageWithoutHtml = htmlRegex.Replace(currentMessage.text, (m) => m.Groups[1].Value);

            foreach (int i in EmojiUtilities.IndexOfAll(messageWithoutHtml, Char.ConvertFromUtf32(imageInfo.swapChar)))
            {
                CustomImage image = null, shadow = null;
                try
                {
                    if (i > 0 && i < currentMessage.text.Length)
                    {
                        image = ChatHandler.Instance.imagePool.Alloc();
                        image.cachedTextureData = cachedTextureData;
                        image.spriteIndex       = imageInfo.textureIndex;
                        image.imageType         = imageInfo.imageType;
                        image.sprite            = cachedTextureData.sprite;
                        image.preserveAspect    = false;
                        if (image.sprite)
                        {
                            image.sprite.texture.wrapMode = TextureWrapMode.Clamp;
                        }

                        image.rectTransform.SetParent(currentMessage.rectTransform, false);
                        image.rectTransform.localScale = new Vector3(0.064f, 0.064f, 0.064f);
                        image.rectTransform.sizeDelta  = new Vector2(cachedTextureData.width, cachedTextureData.height);

                        if (imageInfo.imageType == ImageType.Emoji || imageInfo.imageType == ImageType.Badge)
                        {
                            image.rectTransform.pivot = new Vector2(0, 0.21f);
                        }
                        else
                        {
                            image.rectTransform.pivot = new Vector2(0, 0.3f);
                        }

                        TextGenerator textGen = currentMessage.cachedTextGenerator;
                        Vector3       pos     = new Vector3(textGen.verts[i * 4].position.x, textGen.verts[i * 4].position.y);
                        image.rectTransform.position = currentMessage.gameObject.transform.TransformPoint(pos / pixelsPerUnit + new Vector3(0, 0, -0.1f));

                        if (animatedEmote)
                        {
                            cachedTextureData.animInfo?.animData?.IncRefs();
                            image.material = cachedTextureData.animInfo.imageMaterial;
                            if (ChatConfig.instance.DrawShadows && cachedTextureData.animInfo.shadowMaterial != null)
                            {
                                // Add a shadow to our animated image (the regular unity shadows won't work with this material)
                                shadow                          = ChatHandler.Instance.imagePool.Alloc();
                                shadow.material                 = cachedTextureData.animInfo.shadowMaterial;
                                shadow.sprite                   = null;
                                shadow.spriteIndex              = imageInfo.textureIndex;
                                shadow.imageType                = imageInfo.imageType;
                                shadow.rectTransform.pivot      = new Vector2(0, 0);
                                shadow.rectTransform.localScale = image.rectTransform.localScale;
                                shadow.rectTransform.SetParent(currentMessage.rectTransform, false);
                                shadow.rectTransform.position       = image.rectTransform.position;
                                shadow.rectTransform.localPosition += new Vector3(0.6f, -0.6f, 0.05f);
                                shadow.enabled = true;
                                currentMessage.emoteRenderers.Add(shadow);
                            }
                        }
                        else
                        {
                            image.material = noGlowMaterialUI;
                            if (ChatConfig.instance.DrawShadows)
                            {
                                image.shadow.enabled = true;
                            }
                        }
                        image.enabled = true;
                        currentMessage.emoteRenderers.Add(image);
                    }
                }
                catch (Exception e)
                {
                    if (image)
                    {
                        ChatHandler.Instance.imagePool.Free(image);
                    }

                    Plugin.Log($"Exception {e.ToString()} occured when trying to overlay emote at index {i.ToString()}!");
                }
            }
        }
        public static void Parse(ChatMessage chatMessage)
        {
            _messageBuilder.Clear();

            // Emojis
            chatMessage.displayMsg = EmojiUtilities.emojiRegex.Replace(chatMessage.displayMsg, (m) =>
            {
                string emojiIndex    = EmojiUtilities.WebParseEmojiRegExMatchEvaluator(m);
                string replaceString = m.Value;

                // Couldn't find a match, replace it with a space
                if (emojiIndex == String.Empty)
                {
                    return(" ");
                }

                emojiIndex += ".png";
                if (!ImageDownloader.CachedTextures.ContainsKey(emojiIndex))
                {
                    ImageDownloader.Instance.Queue(new TextureDownloadInfo(emojiIndex, ImageType.Emoji, chatMessage.origMessage.id));
                }

                if (!_emojiInfoByName.TryGetValue(emojiIndex, out var emojiInfo))
                {
                    emojiInfo = new EmoteInfo()
                    {
                        imageType    = ImageType.Emoji,
                        swapChar     = imgPlacementDesignator++,
                        swapString   = replaceString,
                        textureIndex = emojiIndex
                    };
                    _emojiInfoByName[emojiIndex] = emojiInfo;
                }
                chatMessage.parsedEmotes[emojiInfo.textureIndex] = emojiInfo;
                var cachedSprite = emojiInfo.GetCachedSprite();
                return($"\ufeff{Char.ConvertFromUtf32(emojiInfo.swapChar)}{emojiInfo.spacingString}");
            });

            _messageBuilder.Append(chatMessage.displayMsg);
            bool isActionMessage = false;

            if (chatMessage.origMessage is TwitchMessage)
            {
                chatMessage.origMessage.Twitch.ParseEmotes(ref imgPlacementDesignator, ref _messageBuilder, out isActionMessage, chatMessage);
            }
            else if (chatMessage.origMessage is YouTubeMessage)
            {
                chatMessage.origMessage.YouTube.Parse(ref imgPlacementDesignator, ref _messageBuilder, chatMessage);
            }
            else if (chatMessage.origMessage is BilibiliMessage)
            {
                /*Plugin.Log("Is bilibli message!");*/
                chatMessage.origMessage.Bilibili.Parse(ref imgPlacementDesignator, ref _messageBuilder, chatMessage);
            }
            /*Plugin.Log("Parse 1");*/
            string displayColor = "";

            try
            {
                displayColor = chatMessage.origMessage.user.color;
            }
            catch (Exception ex) {
                Plugin.Log(ex.ToString());
            }
            /*Plugin.Log("displayColor: " + displayColor);*/
            int nameHash = chatMessage.origMessage.user.displayName.GetHashCode();

            /*Plugin.Log("nameHash: " + nameHash);
             * Plugin.Log("Parse 2");*/
            // If the user doesn't already have a displayColor, generate one and store it for later user
            if (string.IsNullOrEmpty(displayColor) && !_userColors.TryGetValue(nameHash, out displayColor))
            {
                // Generate a random color
                Random rand = new Random(nameHash);
                int    r    = rand.Next(255);
                int    g    = rand.Next(255);
                int    b    = rand.Next(255);

                // Convert it to a pastel color
                System.Drawing.Color pastelColor = Drawing.GetPastelShade(System.Drawing.Color.FromArgb(255, r, g, b));
                int    argb        = ((int)pastelColor.R << 16) + ((int)pastelColor.G << 8) + (int)pastelColor.B;
                string colorString = String.Format("#{0:X6}", argb) + "FF";
                _userColors.Add(nameHash, colorString);
                displayColor = colorString;
            }
            /*Plugin.Log("Parse 3");*/
            // Set the final displayColor to our message
            chatMessage.displayColor = displayColor;
            /*Plugin.Log("Parse 4");*/
            _messageBuilder.Insert(0, $"<color={chatMessage.displayColor}><b>{chatMessage.origMessage.user.displayName}</b>{(!isActionMessage ? "</color>:" : "")} ");
            _messageBuilder.Append($"{(isActionMessage ? "</color>" : "")}");
            /*Plugin.Log("Parse 5");*/
            if (chatMessage.origMessage is TwitchMessage)
            {
                chatMessage.origMessage.Twitch.ParseBadges(ref imgPlacementDesignator, ref _messageBuilder, chatMessage);
            }
            /*Plugin.Log("Parse 6");*/
            // Finally, push the final message into the render queue
            chatMessage.displayMsg = _messageBuilder.ToString();
            /*Plugin.Log("The message in queue is " + chatMessage.displayMsg);*/
            ChatHandler.RenderQueue.Enqueue(chatMessage);
        }
Beispiel #7
0
        public static async void Parse(ChatMessage newChatMessage)
        {
            // Setup local variables
            char             swapChar     = (char)0xE000;
            List <EmoteInfo> parsedEmotes = new List <EmoteInfo>();
            List <BadgeInfo> parsedBadges = new List <BadgeInfo>();

            bool isActionMessage = false;

            if (newChatMessage.origMessage is TwitchMessage)
            {
                // If this is a twitch message, check if they used /me and color it accordingly
                isActionMessage = newChatMessage.displayMsg.Substring(1).StartsWith("ACTION") && newChatMessage.displayMsg[0] == (char)0x1;
                if (isActionMessage)
                {
                    newChatMessage.displayMsg = newChatMessage.displayMsg.TrimEnd((char)0x1).Substring(8);
                }
            }

            StringBuilder emojilessMessageBuilder = new StringBuilder(newChatMessage.displayMsg);
            // Parse and download any emojis included in the message
            var matches = EmojiUtilities.GetEmojisInString(newChatMessage.displayMsg);

            if (matches.Count > 0)
            {
                List <string> foundEmojis = new List <string>();
                foreach (Match m in matches)
                {
                    string emojiIndex    = EmojiUtilities.WebParseEmojiRegExMatchEvaluator(m);
                    string replaceString = m.Value;

                    // Build up a copy of the message with no emojis so we can parse out our twitch emotes properly
                    emojilessMessageBuilder = emojilessMessageBuilder.Replace(m.Value, " ");

                    if (emojiIndex != String.Empty)
                    {
                        emojiIndex += ".png";
                        if (!ImageDownloader.CachedTextures.ContainsKey(emojiIndex))
                        {
                            ImageDownloader.Instance.Queue(new TextureDownloadInfo(emojiIndex, ImageType.Emoji, newChatMessage.origMessage.id));
                        }

                        if (!foundEmojis.Contains(emojiIndex))
                        {
                            foundEmojis.Add(emojiIndex);
                            EmoteInfo swapInfo = new EmoteInfo();
                            swapInfo.imageType    = ImageType.Emoji;
                            swapInfo.isEmoji      = true;
                            swapInfo.swapChar     = swapChar;
                            swapInfo.swapString   = replaceString;
                            swapInfo.textureIndex = emojiIndex;
                            parsedEmotes.Add(swapInfo);
                            swapChar++;
                        }
                    }
                }
                parsedEmotes = parsedEmotes.OrderByDescending(o => o.swapString.Length).ToList();
                Thread.Sleep(5);
            }

            if (newChatMessage.origMessage is TwitchMessage)
            {
                // Parse and download any twitch emotes in the message
                var emotes = _emoteRegex.Matches(newChatMessage.origMessage.Twitch.emotes);
                if (emotes.Count > 0)
                {
                    string emojilessMessage = emojilessMessageBuilder.ToString();
                    foreach (Match e in emotes)
                    {
                        string emoteIndex = $"T{e.Groups["EmoteIndex"].Value}";
                        if (!ImageDownloader.CachedTextures.ContainsKey(emoteIndex))
                        {
                            ImageDownloader.Instance.Queue(new TextureDownloadInfo(emoteIndex, ImageType.Twitch, newChatMessage.origMessage.id));
                        }

                        int    startReplace = Convert.ToInt32(e.Groups["StartIndex"].Value);
                        int    endReplace   = Convert.ToInt32(e.Groups["EndIndex"].Value);
                        string emoteName    = emojilessMessage.Substring(startReplace, endReplace - startReplace + 1);

                        EmoteInfo swapInfo = new EmoteInfo();
                        swapInfo.swapChar     = swapChar;
                        swapInfo.swapString   = emoteName;
                        swapInfo.textureIndex = emoteIndex;
                        swapInfo.imageType    = ImageType.Twitch;
                        parsedEmotes.Add(swapInfo);
                        swapChar++;
                    }
                    Thread.Sleep(5);
                }

                // Parse and download any twitch badges included in the message
                var badges = _badgeRegex.Matches(newChatMessage.origMessage.user.Twitch.badges);
                if (badges.Count > 0)
                {
                    foreach (Match b in badges)
                    {
                        string badgeName  = $"{b.Groups["BadgeName"].Value}{b.Groups["BadgeVersion"].Value}";
                        string badgeIndex = string.Empty;
                        if (ImageDownloader.TwitchBadgeIDs.ContainsKey(badgeName))
                        {
                            badgeIndex = ImageDownloader.TwitchBadgeIDs[badgeName];
                            if (!ImageDownloader.CachedTextures.ContainsKey(badgeIndex))
                            {
                                ImageDownloader.Instance.Queue(new TextureDownloadInfo(badgeIndex, ImageType.Badge, newChatMessage.origMessage.id));
                            }

                            BadgeInfo swapInfo = new BadgeInfo();
                            swapInfo.swapChar     = swapChar;
                            swapInfo.textureIndex = badgeIndex;
                            swapInfo.imageType    = ImageType.Badge;
                            parsedBadges.Add(swapInfo);
                            swapChar++;
                        }
                    }
                    Thread.Sleep(5);
                }

                // Parse and download any BTTV/FFZ emotes and cheeremotes in the message
                string[] msgParts = newChatMessage.displayMsg.Split(' ').Distinct().ToArray();
                foreach (string w in msgParts)
                {
                    string word = w;
                    //Plugin.Log($"WORD: {word}");
                    string    textureIndex = String.Empty;
                    ImageType imageType    = ImageType.None;
                    if (ImageDownloader.BTTVEmoteIDs.ContainsKey(word))
                    {
                        if (ChatConfig.Instance.ShowBTTVEmotes)
                        {
                            textureIndex = $"B{ImageDownloader.BTTVEmoteIDs[word]}";
                            imageType    = ImageType.BTTV;
                        }
                    }
                    else if (ImageDownloader.BTTVAnimatedEmoteIDs.ContainsKey(word))
                    {
                        if (ChatConfig.Instance.ShowBTTVEmotes)
                        {
                            textureIndex = $"AB{ImageDownloader.BTTVAnimatedEmoteIDs[word]}";
                            imageType    = ImageType.BTTV_Animated;
                        }
                    }
                    else if (ImageDownloader.FFZEmoteIDs.ContainsKey(word))
                    {
                        if (ChatConfig.Instance.ShowFFZEmotes)
                        {
                            textureIndex = $"F{ImageDownloader.FFZEmoteIDs[word]}";
                            imageType    = ImageType.FFZ;
                        }
                    }
                    else if (newChatMessage.origMessage.Twitch.bits > 0 && EmojiUtilities.cheermoteRegex.IsMatch(word.ToLower()))
                    {
                        Match  match  = EmojiUtilities.cheermoteRegex.Match(word.ToLower());
                        string prefix = match.Groups["Prefix"].Value;
                        if (ImageDownloader.TwitchCheermoteIDs.ContainsKey(prefix))
                        {
                            int    bits = Convert.ToInt32(match.Groups["Value"].Value);
                            string tier = ImageDownloader.TwitchCheermoteIDs[prefix].GetTier(bits);
                            textureIndex = $"{prefix}{tier}";
                            imageType    = ImageType.Cheermote;
                        }
                    }

                    if (imageType != ImageType.None)
                    {
                        if (!ImageDownloader.CachedTextures.ContainsKey(textureIndex))
                        {
                            ImageDownloader.Instance.Queue(new TextureDownloadInfo(textureIndex, imageType, newChatMessage.origMessage.id));
                        }

                        EmoteInfo swapInfo = new EmoteInfo();
                        swapInfo.imageType    = imageType;
                        swapInfo.swapChar     = swapChar;
                        swapInfo.swapString   = word;
                        swapInfo.textureIndex = textureIndex;
                        parsedEmotes.Add(swapInfo);
                        swapChar++;
                    }
                }


                Thread.Sleep(5);
            }
            else if (newChatMessage.origMessage is YouTubeMessage)
            {
                if (!ImageDownloader.CachedTextures.ContainsKey(newChatMessage.origMessage.user.YouTube.profileImageUrl))
                {
                    ImageDownloader.Instance.Queue(new TextureDownloadInfo(newChatMessage.origMessage.user.YouTube.profileImageUrl, ImageType.YouTube_Profile, newChatMessage.origMessage.id, true));
                }

                BadgeInfo swapInfo = new BadgeInfo();
                swapInfo.swapChar     = swapChar;
                swapInfo.textureIndex = newChatMessage.origMessage.user.YouTube.profileImageUrl;
                swapInfo.imageType    = ImageType.YouTube_Profile;
                parsedBadges.Add(swapInfo);
                swapChar++;
            }

            string[] parts = newChatMessage.displayMsg.Split(' ');
            // Replace each emote with a unicode character from a private range; we'll draw the emote at the position of this character later on
            foreach (EmoteInfo e in parsedEmotes.Where(e => !e.isEmoji))
            {
                var    cachedSprite = e.GetCachedSprite();
                string extraInfo    = String.Empty;
                if (e.imageType == ImageType.Cheermote)
                {
                    // Insert an animated cheermote into the message
                    Match  cheermote = EmojiUtilities.cheermoteRegex.Match(e.swapString);
                    string numBits   = cheermote.Groups["Value"].Value;
                    extraInfo = $"\u200A<color={ImageDownloader.TwitchCheermoteIDs[cheermote.Groups["Prefix"].Value].GetColor(Convert.ToInt32(numBits))}>\u200A<size=3><b>{numBits}</b></size></color>\u200A";
                }

                // Replace any instances of the swapString we find in the message
                string replaceString = $"\ufeff{Char.ConvertFromUtf32(e.swapChar)}{e.spacingString}{extraInfo}";
                for (int i = 0; i < parts.Length; i++)
                {
                    if (parts[i] == e.swapString)
                    {
                        parts[i] = replaceString;
                    }
                }
            }

            // Then replace our emojis after all the emotes are handled, since these aren't sensitive to spacing
            StringBuilder sb = new StringBuilder(string.Join(" ", parts));

            foreach (EmoteInfo e in parsedEmotes.Where(e => e.isEmoji))
            {
                var cachedSprite = e.GetCachedSprite();
                sb.Replace(e.swapString, $"{Char.ConvertFromUtf32(e.swapChar)}{e.spacingString}");
            }
            newChatMessage.displayMsg = sb.ToString();

            Thread.Sleep(5);

            //// TODO: Re-add tagging, why doesn't unity have highlighting in its default rich text markup?
            //// Highlight messages that we've been tagged in
            //if (Plugin._twitchUsername != String.Empty && msg.Contains(Plugin._twitchUsername)) {
            //    msg = $"<mark=#ffff0050>{msg}</mark>";
            //}

            string displayColor = newChatMessage.origMessage.user.color;
            int    nameHash     = newChatMessage.origMessage.user.displayName.GetHashCode();

            // If the user doesn't already have a displayColor, generate one and store it for later user
            if (string.IsNullOrEmpty(displayColor) && !_userColors.TryGetValue(nameHash, out displayColor))
            {
                // Generate a random color
                Random rand = new Random(nameHash);
                int    r    = rand.Next(255);
                int    g    = rand.Next(255);
                int    b    = rand.Next(255);

                // Convert it to a pastel color
                System.Drawing.Color pastelColor = Drawing.GetPastelShade(System.Drawing.Color.FromArgb(255, r, g, b));
                int    argb        = ((int)pastelColor.R << 16) + ((int)pastelColor.G << 8) + (int)pastelColor.B;
                string colorString = String.Format("#{0:X6}", argb) + "FF";
                _userColors.Add(nameHash, colorString);
                displayColor = colorString;
            }
            // Set the final displayColor to our message
            newChatMessage.displayColor = displayColor;

            // Add the users name to the message with the correct color
            newChatMessage.displayMsg = $"<color={newChatMessage.displayColor}><b>{newChatMessage.origMessage.user.displayName}</b>{(!isActionMessage?"</color>:":"")} {newChatMessage.displayMsg}{(isActionMessage?"</color>":"")}";

            // Prepend the users badges to the front of the message
            StringBuilder badgeStr = new StringBuilder();

            if (parsedBadges.Count > 0)
            {
                parsedBadges.Reverse();
                foreach (BadgeInfo b in parsedBadges)
                {
                    var cachedSprite = b.GetCachedSprite();
                    badgeStr.Insert(0, $"{Char.ConvertFromUtf32(b.swapChar)}{b.spacingString}\u00A0");
                }
            }
            badgeStr.Insert(0, "\uFEFF");
            badgeStr.Append(newChatMessage.displayMsg);


            // Finally, store our final message, parsedEmotes and parsedBadges; then render the message
            newChatMessage.displayMsg   = badgeStr.ToString();
            newChatMessage.parsedEmotes = parsedEmotes;
            newChatMessage.parsedBadges = parsedBadges;

            ChatHandler.RenderQueue.Enqueue(newChatMessage);
        }
        public static void OverlayImage(CustomText currentMessage, ImageInfo imageInfo)
        {
            CachedSpriteData cachedTextureData = ImageDownloader.CachedTextures.ContainsKey(imageInfo.textureIndex) ? ImageDownloader.CachedTextures[imageInfo.textureIndex] : null;

            // If cachedTextureData is null, the emote will be overlayed at a later time once it's finished being cached
            if (cachedTextureData == null || (cachedTextureData.sprite == null && cachedTextureData.animInfo == null))
            {
                return;
            }

            bool animatedEmote = cachedTextureData.animInfo != null;

            foreach (int i in EmojiUtilities.IndexOfAll(currentMessage.text, Char.ConvertFromUtf32(imageInfo.swapChar)))
            {
                CustomImage image = null, shadow = null;
                try
                {
                    if (i > 0 && i < currentMessage.text.Count())
                    {
                        image                     = ChatHandler.Instance.imagePool.Alloc();
                        image.spriteIndex         = imageInfo.textureIndex;
                        image.imageType           = imageInfo.imageType;
                        image.rectTransform.pivot = new Vector2(0, 0);
                        image.sprite              = cachedTextureData.sprite;
                        image.preserveAspect      = false;
                        if (image.sprite)
                        {
                            image.sprite.texture.wrapMode = TextureWrapMode.Clamp;
                        }

                        image.rectTransform.SetParent(currentMessage.rectTransform, false);

                        float aspectRatio = cachedTextureData.width / cachedTextureData.height;
                        if (aspectRatio > 1)
                        {
                            image.rectTransform.localScale = new Vector3(0.064f * aspectRatio, 0.064f, 0.064f);
                        }
                        else
                        {
                            image.rectTransform.localScale = new Vector3(0.064f, 0.064f, 0.064f);
                        }

                        TextGenerator textGen = currentMessage.cachedTextGenerator;
                        Vector3       pos     = new Vector3(textGen.verts[i * 4 + 3].position.x, textGen.verts[i * 4 + 3].position.y);
                        image.rectTransform.position       = currentMessage.gameObject.transform.TransformPoint(pos / pixelsPerUnit - new Vector3(cachedTextureData.width / pixelsPerUnit + 2.5f, cachedTextureData.height / pixelsPerUnit + 1f) + new Vector3(0, 0, -0.1f));
                        image.rectTransform.localPosition -= new Vector3(imageSpacingWidth / 2.3f, 0);

                        if (animatedEmote)
                        {
                            image.material = cachedTextureData.animInfo.imageMaterial;
                            //image.shadow.enabled = false;
                            if (ChatConfig.Instance.DrawShadows)
                            {
                                // Add a shadow to our animated image (the regular unity shadows won't work with this material)
                                shadow                          = ChatHandler.Instance.imagePool.Alloc();
                                shadow.material                 = cachedTextureData.animInfo.shadowMaterial;
                                shadow.sprite                   = null;
                                shadow.spriteIndex              = imageInfo.textureIndex;
                                shadow.imageType                = imageInfo.imageType;
                                shadow.rectTransform.pivot      = new Vector2(0, 0);
                                shadow.rectTransform.localScale = image.rectTransform.localScale;
                                shadow.rectTransform.SetParent(currentMessage.rectTransform, false);
                                shadow.rectTransform.position       = image.rectTransform.position;
                                shadow.rectTransform.localPosition += new Vector3(0.6f, -0.6f, 0.05f);

                                shadow.enabled = true;
                                currentMessage.emoteRenderers.Add(shadow);
                            }
                        }
                        else
                        {
                            image.material = Drawing.noGlowMaterialUI;
                            if (ChatConfig.Instance.DrawShadows)
                            {
                                image.shadow.enabled = true;
                            }
                        }
                        image.enabled = true;
                        currentMessage.emoteRenderers.Add(image);
                    }
                }
                catch (Exception e)
                {
                    if (image)
                    {
                        ChatHandler.Instance.imagePool.Free(image);
                    }

                    Plugin.Log($"Exception {e.ToString()} occured when trying to overlay emote at index {i.ToString()}!");
                }
            }
        }