private void CreateChatFont() { if (_chatFont != null) { return; } TMP_FontAsset font = null; string fontName = _chatConfig.SystemFontName; if (!FontManager.TryGetTMPFontByFamily(fontName, out font)) { Logger.log.Error($"Could not find font {fontName}! Falling back to Segoe UI"); fontName = "Segoe UI"; } font.material.shader = BeatSaberUtils.TMPNoGlowFontShader; _chatFont = new EnhancedFontInfo(font); foreach (var msg in _messages) { msg.Text.SetAllDirty(); if (msg.SubTextEnabled) { msg.SubText.SetAllDirty(); } } while (_backupMessageQueue.TryDequeue(out var msg)) { OnTextMessageReceived(msg); } }
/// <summary> /// This function *blocks* the calling thread, and caches all the images required to display the message, then registers them with the provided font. /// </summary> /// <param name="msg">The chat message to get images from</param> /// <param name="font">The font to register these images to</param> public async Task <bool> PrepareImages(IESCChatMessage msg, EnhancedFontInfo font) { var tasks = new List <Task <EnhancedImageInfo> >(); var pendingEmoteDownloads = new HashSet <string>(); foreach (var emote in msg.Emotes) { if (string.IsNullOrEmpty(emote.Id) || pendingEmoteDownloads.Contains(emote.Id)) { continue; } if (!font.CharacterLookupTable.ContainsKey(emote.Id)) { pendingEmoteDownloads.Add(emote.Id); var tcs = new TaskCompletionSource <EnhancedImageInfo>(); SharedCoroutineStarter.instance.StartCoroutine(this._chatImageProvider.TryCacheSingleImage(emote.Id, emote.Url, emote.Animated ? ChatImageProvider.ESCAnimationType.GIF : ChatImageProvider.ESCAnimationType.MAYBE_GIF, (info) => { if (info == null || !font.TryRegisterImageInfo(info, out var character)) { Logger.Warn($"Failed to register emote \"{emote.Id}\" in font {font.Font.name}."); } tcs.SetResult(info); }, forcedHeight: 110)); tasks.Add(tcs.Task); } } if (msg.Sender is TwitchUser twitchUser) { foreach (var badge in twitchUser.Badges) { if (string.IsNullOrEmpty(badge.Id) || pendingEmoteDownloads.Contains(badge.Id)) { continue; } if (!font.CharacterLookupTable.ContainsKey(badge.Id)) { pendingEmoteDownloads.Add(badge.Id); var tcs = new TaskCompletionSource <EnhancedImageInfo>(); SharedCoroutineStarter.instance.StartCoroutine(this._chatImageProvider.TryCacheSingleImage(badge.Id, badge.Uri, ChatImageProvider.ESCAnimationType.NONE, (info) => { if (info != null) { if (!font.TryRegisterImageInfo(info, out var character)) { Logger.Warn($"Failed to register badge \"{badge.Id}\" in font {font.Font.name}."); } } tcs.SetResult(info); }, forcedHeight: 100)); tasks.Add(tcs.Task); } } } // Wait on all the resources to be ready var result = await Task.WhenAll(tasks); return(result.All(x => x != null)); }
public static async Task <string> BuildMessage(IChatMessage msg, EnhancedFontInfo font) { try { if (!PrepareImages(msg, font)) { Logger.log.Warn($"Failed to prepare some/all images for msg \"{msg.Message}\"!"); //return msg.Message; } ConcurrentStack <EnhancedImageInfo> badges = new ConcurrentStack <EnhancedImageInfo>(); foreach (var badge in msg.Sender.Badges) { if (!ChatImageProvider.instance.CachedImageInfo.TryGetValue(badge.Id, out var badgeInfo)) { Logger.log.Warn($"Failed to find cached image info for badge \"{badge.Id}\"!"); continue; } badges.Push(badgeInfo); } StringBuilder sb = new StringBuilder(msg.Message); // Replace all instances of < with a zero-width non-breaking character foreach (var emote in msg.Emotes) { if (!ChatImageProvider.instance.CachedImageInfo.TryGetValue(emote.Id, out var replace)) { Logger.log.Warn($"Emote {emote.Name} was missing from the emote dict! The request to {emote.Uri} may have timed out?"); continue; } //Logger.log.Info($"Emote: {emote.Name}, StartIndex: {emote.StartIndex}, EndIndex: {emote.EndIndex}, Len: {sb.Length}"); if (!font.TryGetCharacter(replace.ImageId, out uint character)) { Logger.log.Warn($"Emote {emote.Name} was missing from the character dict! Font hay have run out of usable characters."); continue; } try { // Replace emotes by index, in reverse order (msg.Emotes is sorted by emote.StartIndex in descending order) sb.Replace(emote.Name, emote switch { TwitchEmote t when t.Bits > 0 => $"{char.ConvertFromUtf32((int)character)}\u00A0<color={t.Color}><size=77%><b>{t.Bits}\u00A0</b></size></color>", _ => char.ConvertFromUtf32((int)character) }, emote.StartIndex, emote.EndIndex - emote.StartIndex + 1); } catch (Exception ex) { Logger.log.Error($"An unknown error occurred while trying to swap emote {emote.Name} into string of length {sb.Length} at location ({emote.StartIndex}, {emote.EndIndex})"); } }
protected override void OnDestroy() { base.OnDestroy(); ChatConfig.instance.OnConfigChanged -= Instance_OnConfigChanged; BSEvents.menuSceneActive -= BSEvents_menuSceneActive; BSEvents.gameSceneActive -= BSEvents_gameSceneActive; StopAllCoroutines(); foreach (var msg in _messages) { msg.OnLatePreRenderRebuildComplete -= OnRenderRebuildComplete; if (msg.Text.ChatMessage != null) { _backupMessageQueue.Enqueue(msg.Text.ChatMessage); } if (msg.SubText.ChatMessage != null) { _backupMessageQueue.Enqueue(msg.SubText.ChatMessage); } Destroy(msg); } _messages.Clear(); Destroy(_rootGameObject); if (TextPool != null) { TextPool.Dispose(); TextPool = null; } if (_chatScreen != null) { Destroy(_chatScreen); _chatScreen = null; } if (_chatFont != null) { Destroy(_chatFont.Font); _chatFont = null; } if (_chatMoverMaterial != null) { Destroy(_chatMoverMaterial); _chatMoverMaterial = null; } }
/// <summary> /// This function *blocks* the calling thread, and caches all the images required to display the message, then registers them with the provided font. /// </summary> /// <param name="msg">The chat message to get images from</param> /// <param name="font">The font to register these images to</param> public static bool PrepareImages(IChatMessage msg, EnhancedFontInfo font) { List <Task <EnhancedImageInfo> > tasks = new List <Task <EnhancedImageInfo> >(); HashSet <string> pendingEmoteDownloads = new HashSet <string>(); foreach (var emote in msg.Emotes) { if (pendingEmoteDownloads.Contains(emote.Id)) { continue; } if (!font.CharacterLookupTable.ContainsKey(emote.Id)) { pendingEmoteDownloads.Add(emote.Id); TaskCompletionSource <EnhancedImageInfo> tcs = new TaskCompletionSource <EnhancedImageInfo>(); switch (emote.Type) { case EmoteType.SingleImage: SharedCoroutineStarter.instance.StartCoroutine(ChatImageProvider.instance.TryCacheSingleImage(emote.Id, emote.Uri, emote.IsAnimated, (info) => { if (info != null) { if (!font.TryRegisterImageInfo(info, out var character)) { Logger.log.Warn($"Failed to register emote \"{emote.Id}\" in font {font.Font.name}."); } } tcs.SetResult(info); }, forcedHeight: 110)); break; case EmoteType.SpriteSheet: ChatImageProvider.instance.TryCacheSpriteSheetImage(emote.Id, emote.Uri, emote.UVs, (info) => { if (info != null) { if (!font.TryRegisterImageInfo(info, out var character)) { Logger.log.Warn($"Failed to register emote \"{emote.Id}\" in font {font.Font.name}."); } } tcs.SetResult(info); }, forcedHeight: 110); break; default: tcs.SetResult(null); break; } tasks.Add(tcs.Task); } } foreach (var badge in msg.Sender.Badges) { if (pendingEmoteDownloads.Contains(badge.Id)) { continue; } if (!font.CharacterLookupTable.ContainsKey(badge.Id)) { pendingEmoteDownloads.Add(badge.Id); TaskCompletionSource <EnhancedImageInfo> tcs = new TaskCompletionSource <EnhancedImageInfo>(); SharedCoroutineStarter.instance.StartCoroutine(ChatImageProvider.instance.TryCacheSingleImage(badge.Id, badge.Uri, false, (info) => { if (info != null) { if (!font.TryRegisterImageInfo(info, out var character)) { Logger.log.Warn($"Failed to register badge \"{badge.Id}\" in font {font.Font.name}."); } } tcs.SetResult(info); }, forcedHeight: 100)); tasks.Add(tcs.Task); } } // Wait on all the resources to be ready return(Task.WaitAll(tasks.ToArray(), 15000)); }
public async Task <string> BuildMessage(IESCChatMessage msg, EnhancedFontInfo font, BuildMessageTarget buildMessage) { try { if (!await this.PrepareImages(msg, font)) { Logger.Warn($"Failed to prepare some/all images for msg \"{msg.Message}\"!"); //return msg.Message; } var badges = new Stack <EnhancedImageInfo>(); if (msg.Sender is TwitchUser twitchUser) { foreach (var badge in twitchUser.Badges) { if (!this._chatImageProvider.CachedImageInfo.TryGetValue(badge.Id, out var badgeInfo)) { Logger.Warn($"Failed to find cached image info for badge \"{badge.Id}\"!"); continue; } badges.Push(badgeInfo); } } var sb = buildMessage == BuildMessageTarget.Main ? new StringBuilder(msg.Message) : new StringBuilder(msg.SubMessage); // Replace all instances of < with a zero-width non-breaking character foreach (var emote in msg.Emotes) { if (emote is TwitchEmote twitchEmote && 0 < twitchEmote.Bits) { continue; } if (!this._chatImageProvider.CachedImageInfo.TryGetValue(emote.Id, out var replace)) { Logger.Warn($"Emote {emote.Name} was missing from the emote dict! The request to {emote.Url} may have timed out?"); continue; } //Logger.Info($"replase id {replace.ImageId}"); //Logger.Info($"Emote: {emote.Name}, StartIndex: {emote.StartIndex}, EndIndex: {emote.EndIndex}, Len: {sb.Length}"); if (!font.TryGetCharacter(replace.ImageId, out var character)) { Logger.Warn($"Emote {emote.Name} was missing from the character dict! Font hay have run out of usable characters."); continue; } //Logger.Info($"target char {character}"); try { // Replace emotes by index, in reverse order (msg.Emotes is sorted by emote.StartIndex in descending order) if (Regex.IsMatch(emote.Id, "^Emoji_")) { var charIndexText = Regex.Replace(emote.Id, "^Emoji_", ""); var emojiChars = charIndexText.Split('-').Select(x => char.ConvertFromUtf32(Convert.ToInt32($"0x{x}", 16))); var emojiBuilder = new StringBuilder(); foreach (var emojiChar in emojiChars) { emojiBuilder.Append(emojiChar); } sb.Replace(emojiBuilder.ToString(), char.ConvertFromUtf32((int)character)); } else { sb.Replace(emote.Name, char.ConvertFromUtf32((int)character)); } } catch (Exception ex) { Logger.Error($"An unknown error occurred while trying to swap emote {emote.Name} into string of length {sb.Length} at location ({emote.StartIndex}, {emote.EndIndex})\r\n{ex}"); } } sb.Replace("<", "<\u2060"); foreach (var emote in msg.Emotes) { if (emote is not TwitchEmote twitchEmote || twitchEmote.Bits == 0) { continue; } if (!this._chatImageProvider.CachedImageInfo.TryGetValue(emote.Id, out var replace)) { Logger.Warn($"Emote {emote.Name} was missing from the emote dict! The request to {emote.Url} may have timed out?"); continue; } //Logger.Info($"replase id {replace.ImageId}"); //Logger.Info($"Emote: {emote.Name}, StartIndex: {emote.StartIndex}, EndIndex: {emote.EndIndex}, Len: {sb.Length}"); if (!font.TryGetCharacter(replace.ImageId, out var character)) { Logger.Warn($"Emote {emote.Name} was missing from the character dict! Font hay have run out of usable characters."); continue; } //Logger.Info($"target char {character}"); try { // Replace emotes by index, in reverse order (msg.Emotes is sorted by emote.StartIndex in descending order) sb.Replace(emote.Name, $"{char.ConvertFromUtf32((int)character)}\u00A0<color={twitchEmote.Color}><size=77%><b>{twitchEmote.Bits}\u00A0</b></size></color>"); } catch (Exception ex) { Logger.Error($"An unknown error occurred while trying to swap emote {emote.Name} into string of length {sb.Length} at location ({emote.StartIndex}, {emote.EndIndex})\r\n{ex}"); } } if (buildMessage == BuildMessageTarget.Main && msg.IsSystemMessage) { // System messages get a grayish color to differenciate them from normal messages in chat, and do not receive a username/badge prefix sb.Insert(0, $"<color=#bbbbbbff>"); sb.Append("</color>"); } else { var nameColorCode = msg.Sender?.Color; if (ColorUtility.TryParseHtmlString(nameColorCode?.Substring(0, 7), out var nameColor)) { if (nameColor == Color.white && !s_senderColor.TryGetValue(msg.Sender?.UserName, out nameColor)) { nameColor = new Color(((float)this._random.Next(0, 100000) / 100000), ((float)this._random.Next(0, 100000) / 100000), ((float)this._random.Next(0, 100000) / 100000)); s_senderColor.TryAdd(msg.Sender?.UserName, nameColor); } Color.RGBToHSV(nameColor, out var h, out var s, out var v); if (v < 0.85f) { v = 0.85f; nameColor = Color.HSVToRGB(h, s, v); } nameColorCode = ColorUtility.ToHtmlStringRGBA(nameColor); nameColorCode = nameColorCode.Insert(0, "#"); } if (msg.IsActionMessage) { // Message becomes the color of their name if it's an action message sb.Insert(0, $"<color={nameColorCode}><b>{msg.Sender?.DisplayName}</b> "); sb.Append("</color>"); } else { // Insert username w/ color sb.Insert(0, $"<color={nameColorCode}><b>{msg.Sender?.DisplayName}</b></color>: "); } var parsedBadge = new HashSet <string>(); if (msg.Sender is TwitchUser twitchUser1) { for (var i = 0; i < twitchUser1.Badges.Count; i++) { // Insert user badges at the beginning of the string in reverse order var badge = badges.Pop(); if (parsedBadge.Contains(badge.ImageId)) { continue; } parsedBadge.Add(badge.ImageId); if (badge != null && font.TryGetCharacter(badge.ImageId, out var character)) { sb.Insert(0, $"{char.ConvertFromUtf32((int)character)} "); } else { Logger.Warn("Undefind badge"); } } } } return(sb.ToString()); } catch (Exception ex) { Logger.Error($"An exception occurred in ChatMessageBuilder while parsing msg with {msg.Emotes.Count} emotes. Msg: \"{msg.Message}\". {ex}"); } return(msg.Message); }
public static Task <string> BuildMessage(IChatMessage msg, EnhancedFontInfo font) => Task.Run(() => { try { if (!PrepareImages(msg, font))