Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of TwitchDrones v1.0.0
TwitchDrones.dll
Decompiled 11 hours agousing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.WebSockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using RiskOfOptions; using RiskOfOptions.Options; using RoR2; using RoR2.ExpansionManagement; using RoR2.UI; using UnityEngine; using UnityEngine.Events; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("TwitchDrones")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("TwitchDrones")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("cf10b9e7-8f20-49f6-8ca3-1aa19a314cdb")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] namespace TwitchDrones; [Serializable] public class RewardInfo { public string id; public string title; } [Serializable] public class RedemptionEvent { public string id; public string user_id; public string user_login; public string user_name; public string user_input; public RewardInfo reward; } [Serializable] public class RedemptionPayload { public RedemptionEvent @event; } [Serializable] public class RedemptionMessage { public RedemptionPayload payload; } public class DroneReward { public string Name; public int Cost; public string Master; } public class PendingSpawn { public string UserId; public string UserName; public string Master; public string RedemptionId; public string RewardId; } public class DroneEntry { public CharacterMaster Master; public string UserName; public string UserId; } public class ChatterInfo { public string Id; public string Name; } public class ChatLine { public string UserId; public string Text; public float Time; } [BepInPlugin("com.user.twitchdrones", "TwitchDrones", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class TwitchDronesPlugin : BaseUnityPlugin { private const string DefaultClientId = "qnxo638gz6qgwexb5oe74k6bq1l15m"; private const string BuiltInExclude = "Backup,Operator,Оператор,DOC,Док,CROSSHAIRS,Мушка,CHIRP,Пташка"; private const string Scopes = "channel:read:redemptions channel:manage:redemptions user:read:chat moderator:read:chatters"; private string clientId; private string accessToken = ""; private string refreshToken = ""; private string randomExclude; private string broadcasterId; private bool rewardsEnsured; private bool poolLogged; private bool authRunning; private ConfigEntry<bool> enabledEntry; private readonly object authLock = new object(); private readonly List<DroneReward> rewards = new List<DroneReward>(); private readonly ConcurrentQueue<string> notifications = new ConcurrentQueue<string>(); private readonly ConcurrentQueue<Action> mainThread = new ConcurrentQueue<Action>(); private readonly List<PendingSpawn> pending = new List<PendingSpawn>(); private readonly List<DroneEntry> droneList = new List<DroneEntry>(); private readonly HashSet<string> usersWithDrone = new HashSet<string>(); private ConfigEntry<bool> removeOnExitEntry; private ConfigEntry<int> nameSizeEntry; private ConfigEntry<int> nameDistanceEntry; private bool cameraWarned; private bool chattersWarned; private float scanTimer; private bool initialized; private bool isRussian; private string authFilePath; private readonly ConcurrentDictionary<string, string> rewardIds = new ConcurrentDictionary<string, string>(); private ConfigEntry<bool> pauseOutsideRunEntry; private int pauseState = -1; private bool pauseBusy; private bool pauseFailed; private float pauseRetryAt; private string chatterExclude; private readonly object chatterLock = new object(); private readonly List<ChatterInfo> chatters = new List<ChatterInfo>(); private readonly ConcurrentQueue<ChatLine> chatLines = new ConcurrentQueue<ChatLine>(); private readonly Dictionary<string, ChatLine> lastMessages = new Dictionary<string, ChatLine>(); private ConfigEntry<bool> showMessagesEntry; private ConfigEntry<bool> ignoreCommandsEntry; private ConfigEntry<int> messageSecondsEntry; private ConfigEntry<int> messageLengthEntry; private ConfigEntry<int> messageSizeEntry; private GUIStyle messageStyle; private Texture2D bgTex; private readonly HashSet<CharacterMaster> seenMasters = new HashSet<CharacterMaster>(); private GUIStyle nameStyle; private GUIStyle shadowStyle; private CancellationTokenSource twitchCts; private string AuthFilePath => authFilePath; private void Awake() { RoR2Application.onLoad = (Action)Delegate.Combine(RoR2Application.onLoad, new Action(Init)); } private void Init() { RoR2Application.onLoad = (Action)Delegate.Remove(RoR2Application.onLoad, new Action(Init)); isRussian = DetectRussian(); clientId = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch", "ClientId", "qnxo638gz6qgwexb5oe74k6bq1l15m", T("Client ID приложения с dev.twitch.tv", "Client ID of your app from dev.twitch.tv")).Value.Trim(); enabledEntry = ((BaseUnityPlugin)this).Config.Bind<bool>("Twitch", T("Включить интеграцию с Twitch", "Enable Twitch integration"), false, T("Включено: мод создаёт награды на канале и слушает покупки. Выключено: награды удаляются с канала.", "On: the mod creates rewards on your channel and listens for redemptions. Off: rewards are removed from the channel.")); removeOnExitEntry = ((BaseUnityPlugin)this).Config.Bind<bool>("Twitch", T("Удалять награды при выходе из игры", "Remove rewards on game exit"), true, T("Чтобы зрители не тратили баллы, пока игра закрыта. При следующем запуске награды создадутся заново.", "So viewers do not spend points while the game is closed. Rewards are created again on the next launch.")); pauseOutsideRunEntry = ((BaseUnityPlugin)this).Config.Bind<bool>("Twitch", T("Ставить награды на паузу вне забега", "Pause rewards outside a run"), true, T("Когда ты в меню или лобби, награды на канале стоят на паузе, чтобы зрители не тратили баллы зря.", "While you are in the menu or lobby, rewards on the channel are paused so viewers do not waste points.")); nameSizeEntry = ((BaseUnityPlugin)this).Config.Bind<int>("NameTag", "Size", 20, T("Размер шрифта подписи над дроном", "Font size of the name above the drone")); nameDistanceEntry = ((BaseUnityPlugin)this).Config.Bind<int>("NameTag", "MaxDistance", 100, T("Дальше этого расстояния (в метрах) подпись не показывается", "Names farther than this distance (in meters) are hidden")); showMessagesEntry = ((BaseUnityPlugin)this).Config.Bind<bool>("Messages", T("Показывать сообщения чатеров", "Show chat messages"), true, T("Сообщения владельца дрона из чата Twitch показываются над его дроном.", "Messages from the drone owner in Twitch chat are shown above their drone.")); ignoreCommandsEntry = ((BaseUnityPlugin)this).Config.Bind<bool>("Messages", "IgnoreCommands", true, T("Не показывать сообщения, которые начинаются с ! (команды ботов)", "Hide messages that start with ! (bot commands)")); messageSecondsEntry = ((BaseUnityPlugin)this).Config.Bind<int>("Messages", "Seconds", 8, T("Сколько секунд сообщение висит над дроном", "How many seconds a message stays above the drone")); messageLengthEntry = ((BaseUnityPlugin)this).Config.Bind<int>("Messages", "MaxLength", 120, T("Максимальная длина сообщения (длинные обрезаются)", "Maximum message length (longer ones are cut)")); messageSizeEntry = ((BaseUnityPlugin)this).Config.Bind<int>("Messages", "Size", 16, T("Размер шрифта сообщения", "Message font size")); AddReward(1, T("Дрон-стрелок", "Gunner Drone"), 100, "Drone1Master"); AddReward(2, T("Дрон-медик", "Healing Drone"), 150, "Drone2Master"); AddReward(3, T("Случайный дрон", "Random Drone"), 250, "random"); randomExclude = ((BaseUnityPlugin)this).Config.Bind<string>("Drone", "RandomExcludeContains", "", T("Через запятую: ДОПОЛНИТЕЛЬНЫЕ слова. Если такое слово есть в названии дрона (внутреннем или игровом), он не попадает в случайный выбор. Operator, DOC, CROSSHAIRS, CHIRP и дроны Back-up исключены всегда.", "Comma-separated EXTRA words. If a drone's name (internal or in-game) contains one of them, it is never picked as a random drone. Operator, DOC, CROSSHAIRS, CHIRP and Back-up drones are always excluded.")).Value; chatterExclude = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch", "ChatterExclude", "nightbot,streamelements,streamlabs,moobot,fossabot,wizebot", T("Через запятую: логины зрителей, которые не получают имена дронов (боты)", "Comma-separated viewer logins that never get a drone name (bots)")).Value.ToLowerInvariant(); Run.onRunStartGlobal += OnRunStart; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; LoadAuth(); SetupMenu(); enabledEntry.SettingChanged += OnEnabledChanged; ApplyEnabled(startup: true); ((BaseUnityPlugin)this).Logger.LogInfo((object)T("TwitchDrones загружен", "TwitchDrones loaded")); initialized = true; } private void SetupMenu() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Expected O, but got Unknown //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Expected O, but got Unknown ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(enabledEntry)); ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(removeOnExitEntry)); ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(pauseOutsideRunEntry)); ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(showMessagesEntry)); ModSettingsManager.AddOption((BaseOption)new GenericButtonOption(T("Авторизация", "Authorization"), "Twitch", T("Откроет страницу Twitch в браузере. Нажми там «Авторизовать», и мод получит доступ к наградам канала.", "Opens the Twitch page in your browser. Click Authorize there and the mod gets access to your channel rewards."), T("Авторизоваться в Twitch", "Authorize with Twitch"), new UnityAction(StartAuth))); ModSettingsManager.AddOption((BaseOption)new GenericButtonOption(T("Список случайных дронов", "Random drone list"), "Drone", T("Показывает, какие дроны могут выпасть по награде «Случайный дрон».", "Shows which drones can be given by the Random Drone reward."), T("Показать список", "Show list"), new UnityAction(ShowPool))); } private void AddReward(int number, string defaultName, int defaultCost, string defaultMaster) { string text = "Reward" + number; DroneReward droneReward = new DroneReward(); droneReward.Name = ((BaseUnityPlugin)this).Config.Bind<string>(text, "Name", defaultName, T("Название награды на Twitch (пусто = награда выключена)", "Reward title on Twitch (empty = reward disabled)")).Value.Trim(); droneReward.Cost = ((BaseUnityPlugin)this).Config.Bind<int>(text, "Cost", defaultCost, T("Стоимость в баллах канала", "Cost in channel points")).Value; droneReward.Master = ((BaseUnityPlugin)this).Config.Bind<string>(text, "Master", defaultMaster, T("Тип дрона: Drone1Master (стрелок), Drone2Master (лечащий), MissileDroneMaster, FlameDroneMaster, EmergencyDroneMaster, EquipmentDroneMaster, MegaDroneMaster, либо random (случайный)", "Drone type: Drone1Master (gunner), Drone2Master (healing), MissileDroneMaster, FlameDroneMaster, EmergencyDroneMaster, EquipmentDroneMaster, MegaDroneMaster, or random")).Value.Trim(); if (droneReward.Name != "") { rewards.Add(droneReward); } } private void OnEnabledChanged(object sender, EventArgs e) { ApplyEnabled(startup: false); } private void ApplyEnabled(bool startup) { if (enabledEntry.Value) { if (accessToken == "" && refreshToken == "") { if (startup) { ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Интеграция включена, но нет авторизации: нажми кнопку авторизации в настройках мода.", "Integration is enabled but not authorized: press the authorize button in the mod settings.")); } else { Notify(T("Сначала нажми кнопку «Авторизоваться в Twitch» в настройках мода.", "First press the Authorize with Twitch button in the mod settings.")); } } else { StartTwitch(); } return; } StopTwitch(); if (!startup) { Task.Run(delegate { DeleteRewards(showMessage: true); }); } } private void OnDestroy() { RoR2Application.onLoad = (Action)Delegate.Remove(RoR2Application.onLoad, new Action(Init)); Run.onRunStartGlobal -= OnRunStart; StopTwitch(); } private void OnApplicationQuit() { if (initialized && removeOnExitEntry.Value && enabledEntry.Value) { StopTwitch(); DeleteRewards(showMessage: false); } } private void OnRunStart(Run run) { droneList.Clear(); usersWithDrone.Clear(); seenMasters.Clear(); lastMessages.Clear(); try { List<string> list = new List<string>(); List<string> list2 = new List<string>(); foreach (GameObject item in BuildPool(checkExpansion: true, list)) { list2.Add(DescribeDrone(item)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Пул случайных дронов (", "Random drone pool (") + list2.Count + "): " + string.Join("; ", list2.ToArray()))); ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Исключены из случайных (", "Excluded from random (") + list.Count + "): " + string.Join("; ", list.ToArray()))); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось составить список дронов: ", "Failed to build the drone list: ") + ex.Message)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Новый забег: список дронов очищен", "New run: drone list cleared")); } private void StartTwitch() { StopTwitch(); rewardsEnsured = false; pauseState = -1; twitchCts = new CancellationTokenSource(); CancellationToken token = twitchCts.Token; Task.Run(() => RunAsync(token)); Task.Run(() => ChattersLoop(token)); ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Подключаюсь к Twitch...", "Connecting to Twitch...")); } private void StopTwitch() { if (twitchCts != null) { twitchCts.Cancel(); twitchCts = null; } rewardIds.Clear(); lock (chatterLock) { chatters.Clear(); } } private bool DetectRussian() { try { return Language.currentLanguageName?.StartsWith("ru", StringComparison.OrdinalIgnoreCase) ?? false; } catch (Exception) { return false; } } private string T(string russian, string english) { return isRussian ? russian : english; } private void Notify(string text) { ((BaseUnityPlugin)this).Logger.LogInfo((object)text); mainThread.Enqueue(delegate { ShowMessage(text); }); } private void ShowMessage(string text) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) try { SimpleDialogBox val = SimpleDialogBox.Create((MPEventSystem)null); val.headerToken = new TokenParamsPair("TwitchDrones", Array.Empty<object>()); val.descriptionToken = new TokenParamsPair(text, Array.Empty<object>()); val.AddCancelButton("OK", Array.Empty<object>()); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось показать окно: ", "Failed to show the dialog: ") + ex.Message)); } } private DroneReward FindReward(string title) { foreach (DroneReward reward in rewards) { if (string.Equals(reward.Name, title, StringComparison.OrdinalIgnoreCase)) { return reward; } } return null; } private void Update() { if (!initialized) { return; } Action result; while (mainThread.TryDequeue(out result)) { try { result(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex.ToString()); } } string result2; while (notifications.TryDequeue(out result2)) { RedemptionMessage redemptionMessage = JsonUtility.FromJson<RedemptionMessage>(result2); if (redemptionMessage != null && redemptionMessage.payload != null && redemptionMessage.payload.@event != null && redemptionMessage.payload.@event.reward != null) { RedemptionEvent redemptionEvent = redemptionMessage.payload.@event; DroneReward droneReward = FindReward(redemptionEvent.reward.title); ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Награда '", "Reward '") + redemptionEvent.reward.title + T("' от ", "' from ") + redemptionEvent.user_name + " (id " + redemptionEvent.user_id + ")" + ((droneReward != null) ? T(" [НАША НАГРАДА]", " [OUR REWARD]") : ""))); if (droneReward != null) { PendingSpawn pendingSpawn = new PendingSpawn(); pendingSpawn.UserId = redemptionEvent.user_id; pendingSpawn.UserName = redemptionEvent.user_name; pendingSpawn.Master = droneReward.Master; pendingSpawn.RedemptionId = redemptionEvent.id; pendingSpawn.RewardId = redemptionEvent.reward.id; pending.Add(pendingSpawn); } } } ChatLine result3; while (chatLines.TryDequeue(out result3)) { if (showMessagesEntry.Value && HasDroneFor(result3.UserId)) { result3.Time = Time.unscaledTime; lastMessages[result3.UserId] = result3; } } scanTimer -= Time.unscaledDeltaTime; if (scanTimer <= 0f) { scanTimer = 1f; isRussian = DetectRussian(); ScanTeamDrones(); UpdatePause(); } if (pending.Count <= 0 || !((Object)(object)GetOwnerBody() != (Object)null)) { return; } foreach (PendingSpawn item in pending) { SpawnDrone(item); } pending.Clear(); } private CharacterBody GetOwnerBody() { if ((Object)(object)Run.instance == (Object)null || PlayerCharacterMasterController.instances.Count == 0) { return null; } CharacterMaster master = PlayerCharacterMasterController.instances[0].master; return ((Object)(object)master != (Object)null) ? master.GetBody() : null; } private Camera GetCamera() { return Camera.main; } private bool HasDroneFor(string userId) { foreach (DroneEntry drone in droneList) { if (drone.UserId == userId) { return true; } } return false; } private void DrawMessage(ChatLine chat, Rect nameRect) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) float num = Time.unscaledTime - chat.Time; float num2 = messageSecondsEntry.Value; if (!(num > num2)) { string text = chat.Text; int num3 = Mathf.Max(10, messageLengthEntry.Value); if (text.Length > num3) { text = text.Substring(0, num3) + "…"; } float num4 = Mathf.Clamp01(num2 - num); GUIContent val = new GUIContent(text); Vector2 val2 = messageStyle.CalcSize(val); float num5 = Mathf.Min(val2.x + 20f, 320f); float num6 = messageStyle.CalcHeight(val, num5 - 12f) + 8f; Rect val3 = default(Rect); ((Rect)(ref val3))..ctor(((Rect)(ref nameRect)).center.x - num5 / 2f, ((Rect)(ref nameRect)).y - 4f - num6, num5, num6); Color color = GUI.color; GUI.color = new Color(1f, 1f, 1f, num4); GUI.DrawTexture(val3, (Texture)(object)bgTex); GUI.Label(new Rect(((Rect)(ref val3)).x + 6f, ((Rect)(ref val3)).y + 4f, num5 - 12f, num6 - 8f), val, messageStyle); GUI.color = color; } } private void OnGUI() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Invalid comparison between Unknown and I4 //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Expected O, but got Unknown //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Expected O, but got Unknown //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Expected O, but got Unknown //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Unknown result type (might be due to invalid IL or missing references) //IL_028d: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Unknown result type (might be due to invalid IL or missing references) //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02a1: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Unknown result type (might be due to invalid IL or missing references) //IL_02a8: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02df: Unknown result type (might be due to invalid IL or missing references) //IL_02eb: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Unknown result type (might be due to invalid IL or missing references) //IL_034f: Unknown result type (might be due to invalid IL or missing references) //IL_039a: Unknown result type (might be due to invalid IL or missing references) if (!initialized || droneList.Count == 0 || (int)Event.current.type != 7) { return; } Camera camera = GetCamera(); if ((Object)(object)camera == (Object)null) { if (!cameraWarned) { cameraWarned = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)T("Не найдена основная камера, подписи над дронами не показываются", "Main camera not found, names above drones are not shown")); } return; } if (nameStyle == null || nameStyle.fontSize != nameSizeEntry.Value) { nameStyle = new GUIStyle(GUI.skin.label); nameStyle.alignment = (TextAnchor)4; nameStyle.fontStyle = (FontStyle)1; nameStyle.fontSize = nameSizeEntry.Value; nameStyle.wordWrap = false; nameStyle.normal.textColor = new Color(1f, 0.95f, 0.6f); shadowStyle = new GUIStyle(nameStyle); shadowStyle.normal.textColor = Color.black; } if (messageStyle == null || messageStyle.fontSize != messageSizeEntry.Value) { messageStyle = new GUIStyle(GUI.skin.label); messageStyle.alignment = (TextAnchor)4; messageStyle.fontSize = messageSizeEntry.Value; messageStyle.wordWrap = true; messageStyle.normal.textColor = Color.white; } if ((Object)(object)bgTex == (Object)null) { bgTex = new Texture2D(1, 1); bgTex.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.65f)); bgTex.Apply(); } Rect val2 = default(Rect); foreach (DroneEntry drone in droneList) { if ((Object)(object)drone.Master == (Object)null) { continue; } CharacterBody body = drone.Master.GetBody(); if ((Object)(object)body == (Object)null) { continue; } Vector3 val = camera.WorldToScreenPoint(((Component)body).transform.position + Vector3.up * 1.8f); if (!(val.z <= 0f) && !(val.z > (float)nameDistanceEntry.Value)) { float num = (float)Screen.height - val.y; ((Rect)(ref val2))..ctor(val.x - 150f, num - 15f, 300f, 30f); GUI.Label(new Rect(((Rect)(ref val2)).x + 1.5f, ((Rect)(ref val2)).y + 1.5f, ((Rect)(ref val2)).width, ((Rect)(ref val2)).height), drone.UserName, shadowStyle); GUI.Label(val2, drone.UserName, nameStyle); if (showMessagesEntry.Value && drone.UserId != null && lastMessages.TryGetValue(drone.UserId, out var value)) { DrawMessage(value, val2); } } } } private void SpawnDrone(PendingSpawn p) { //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Expected O, but got Unknown //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) if (usersWithDrone.Contains(p.UserId)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("У ", "Viewer ") + p.UserName + T(" уже есть дрон в этом забеге, баллы возвращены", " already has a drone in this run, points refunded"))); SetRedemptionStatus(p.RedemptionId, p.RewardId, "CANCELED"); return; } CharacterBody ownerBody = GetOwnerBody(); if ((Object)(object)ownerBody == (Object)null) { return; } GameObject val = ((!string.Equals(p.Master, "random", StringComparison.OrdinalIgnoreCase)) ? MasterCatalog.FindMasterPrefab(p.Master) : PickRandomDrone()); if ((Object)(object)val == (Object)null) { ((BaseUnityPlugin)this).Logger.LogError((object)(T("Не найден тип дрона '", "Drone type not found: '") + p.Master + T("', проверь настройку Master в конфиге", "', check the Master setting in the config"))); SetRedemptionStatus(p.RedemptionId, p.RewardId, "CANCELED"); return; } MasterSummon val2 = new MasterSummon(); val2.masterPrefab = val; val2.position = ((Component)ownerBody).transform.position + Vector3.up * 2f; val2.rotation = Quaternion.identity; val2.summonerBodyObject = ((Component)ownerBody).gameObject; val2.ignoreTeamMemberLimit = true; CharacterMaster val3 = val2.Perform(); if ((Object)(object)val3 == (Object)null) { ((BaseUnityPlugin)this).Logger.LogError((object)(T("Не удалось создать дрона для ", "Failed to create a drone for ") + p.UserName)); SetRedemptionStatus(p.RedemptionId, p.RewardId, "CANCELED"); return; } usersWithDrone.Add(p.UserId); DroneEntry droneEntry = new DroneEntry(); droneEntry.Master = val3; droneEntry.UserName = p.UserName; droneEntry.UserId = p.UserId; droneList.Add(droneEntry); seenMasters.Add(val3); CharacterBody body = val3.GetBody(); if ((Object)(object)body != (Object)null) { body.baseNameToken = p.UserName; } ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Дрон ", "Drone ") + ((Object)val).name + T(" для ", " for ") + p.UserName + T(" создан", " created"))); SetRedemptionStatus(p.RedemptionId, p.RewardId, "FULFILLED"); } private bool IsExcluded(CharacterMaster m) { List<string> list = new List<string>(); list.Add(((Object)((Component)m).gameObject).name); if ((Object)(object)m.bodyPrefab != (Object)null) { list.Add(((Object)m.bodyPrefab).name); CharacterBody component = m.bodyPrefab.GetComponent<CharacterBody>(); if ((Object)(object)component != (Object)null) { list.Add(component.baseNameToken); list.Add(Language.GetString(component.baseNameToken)); } } string[] array = ("Backup,Operator,Оператор,DOC,Док,CROSSHAIRS,Мушка,CHIRP,Пташка," + randomExclude).Split(new char[1] { ',' }); foreach (string text in array) { string text2 = text.Trim(); if (text2 == "") { continue; } foreach (string item in list) { if (!string.IsNullOrEmpty(item) && item.IndexOf(text2, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } return false; } private List<GameObject> BuildPool(bool checkExpansion, List<string> excluded) { List<GameObject> list = new List<GameObject>(); foreach (CharacterMaster allAiMaster in MasterCatalog.allAiMasters) { string name = ((Object)((Component)allAiMaster).gameObject).name; if (name.IndexOf("Drone", StringComparison.OrdinalIgnoreCase) < 0 || (Object)(object)allAiMaster.bodyPrefab == (Object)null) { continue; } if (IsExcluded(allAiMaster)) { excluded?.Add(DescribeDrone(((Component)allAiMaster).gameObject)); continue; } if (checkExpansion && (Object)(object)Run.instance != (Object)null) { ExpansionRequirementComponent component = allAiMaster.bodyPrefab.GetComponent<ExpansionRequirementComponent>(); if ((Object)(object)component != (Object)null && (Object)(object)component.requiredExpansion != (Object)null && !Run.instance.IsExpansionEnabled(component.requiredExpansion)) { continue; } } list.Add(((Component)allAiMaster).gameObject); } return list; } private string DescribeDrone(GameObject masterPrefab) { string text = ((Object)masterPrefab).name; CharacterMaster component = masterPrefab.GetComponent<CharacterMaster>(); if ((Object)(object)component != (Object)null && (Object)(object)component.bodyPrefab != (Object)null) { CharacterBody component2 = component.bodyPrefab.GetComponent<CharacterBody>(); if ((Object)(object)component2 != (Object)null) { text = text + " (" + Language.GetString(component2.baseNameToken) + ")"; } } return text; } private void ShowPool() { bool flag = (Object)(object)Run.instance != (Object)null; List<string> list = new List<string>(); List<string> list2 = new List<string>(); foreach (GameObject item in BuildPool(flag, list)) { list2.Add(DescribeDrone(item)); } string text = (flag ? (T("Дроны, которые могут выпасть (с учётом включённых DLC), всего ", "Drones that can be given (with enabled DLC taken into account), total ") + list2.Count + ": ") : (T("Дроны, которые могут выпасть, всего ", "Drones that can be given, total ") + list2.Count + T(" (DLC не проверялось, зайди в забег для точного списка): ", " (DLC not checked, join a run for the exact list): "))); Notify(text + string.Join("; ", list2.ToArray()) + T(". Исключены (", ". Excluded (") + list.Count + "): " + string.Join("; ", list.ToArray())); } private GameObject PickRandomDrone() { List<GameObject> list = BuildPool(checkExpansion: true, null); if (list.Count == 0) { return null; } return list[Random.Range(0, list.Count)]; } private ChatterInfo PickChatter() { List<ChatterInfo> list; lock (chatterLock) { list = new List<ChatterInfo>(chatters); } if (list.Count == 0) { return null; } List<ChatterInfo> list2 = list.FindAll((ChatterInfo c) => !usersWithDrone.Contains(c.Id)); if (list2.Count == 0) { return null; } HashSet<string> used = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (DroneEntry drone in droneList) { used.Add(drone.UserName); } List<ChatterInfo> list3 = list2.FindAll((ChatterInfo c) => !used.Contains(c.Name)); List<ChatterInfo> list4 = ((list3.Count > 0) ? list3 : list2); return list4[Random.Range(0, list4.Count)]; } private void ScanTeamDrones() { foreach (DroneEntry drone in droneList) { if (!((Object)(object)drone.Master == (Object)null)) { CharacterBody body = drone.Master.GetBody(); if ((Object)(object)body != (Object)null && body.baseNameToken != drone.UserName) { body.baseNameToken = drone.UserName; } } } if ((Object)(object)Run.instance == (Object)null) { return; } foreach (TeamComponent teamMember in TeamComponent.GetTeamMembers((TeamIndex)1)) { CharacterBody body2 = teamMember.body; if ((Object)(object)body2 == (Object)null) { continue; } CharacterMaster master = body2.master; if ((Object)(object)master == (Object)null || (Object)(object)master.playerCharacterMasterController != (Object)null || seenMasters.Contains(master)) { continue; } string name = ((Object)((Component)master).gameObject).name; if (name.IndexOf("Drone", StringComparison.OrdinalIgnoreCase) < 0 || name.IndexOf("Backup", StringComparison.OrdinalIgnoreCase) >= 0) { seenMasters.Add(master); continue; } ChatterInfo chatterInfo = PickChatter(); if (chatterInfo == null) { break; } seenMasters.Add(master); DroneEntry droneEntry = new DroneEntry(); droneEntry.Master = master; droneEntry.UserName = chatterInfo.Name; droneEntry.UserId = chatterInfo.Id; droneList.Add(droneEntry); body2.baseNameToken = chatterInfo.Name; ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Дрон ", "Drone ") + name + T(" получил имя зрителя ", " was named after viewer ") + chatterInfo.Name)); } } private async Task ChattersLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { if (broadcasterId != null) { FetchChatters(); } } catch (Exception ex) { if (!chattersWarned) { chattersWarned = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось получить список зрителей: ", "Failed to get the viewer list: ") + ex.Message)); } } try { await Task.Delay(60000, ct); } catch (OperationCanceledException) { break; } } } private void FetchChatters() { string input = HttpRequest("GET", "https://api.twitch.tv/helix/chat/chatters?broadcaster_id=" + broadcasterId + "&moderator_id=" + broadcasterId + "&first=1000", null); HashSet<string> hashSet = new HashSet<string>(); string[] array = chatterExclude.Split(new char[1] { ',' }); foreach (string text in array) { string text2 = text.Trim(); if (text2 != "") { hashSet.Add(text2); } } List<ChatterInfo> list = new List<ChatterInfo>(); foreach (Match item2 in Regex.Matches(input, "\"user_id\"\\s*:\\s*\"(\\d+)\"\\s*,\\s*\"user_login\"\\s*:\\s*\"([^\"]*)\"\\s*,\\s*\"user_name\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")) { string value = item2.Groups[1].Value; string item = item2.Groups[2].Value.ToLowerInvariant(); string text3 = Regex.Unescape(item2.Groups[3].Value); if (!(value == broadcasterId) && !hashSet.Contains(item) && !(text3 == "")) { ChatterInfo chatterInfo = new ChatterInfo(); chatterInfo.Id = value; chatterInfo.Name = text3; list.Add(chatterInfo); } } lock (chatterLock) { chatters.Clear(); chatters.AddRange(list); } } private void LoadAuth() { authFilePath = Path.Combine(Application.persistentDataPath, "TwitchDrones.auth"); string text = Path.Combine(Paths.ConfigPath, "TwitchDrones.auth"); try { if (!File.Exists(authFilePath) && File.Exists(text)) { File.Copy(text, authFilePath); } if (File.Exists(text)) { File.Delete(text); } if (File.Exists(authFilePath)) { string[] array = File.ReadAllLines(authFilePath); accessToken = ((array.Length != 0) ? array[0].Trim() : ""); refreshToken = ((array.Length > 1) ? array[1].Trim() : ""); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось прочитать файл авторизации: ", "Failed to read the auth file: ") + ex.Message)); } } private void SaveAuth() { try { File.WriteAllLines(AuthFilePath, new string[2] { accessToken, refreshToken }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось сохранить файл авторизации: ", "Failed to save the auth file: ") + ex.Message)); } } private void StartAuth() { if (clientId == "") { Notify(T("Не задан ClientId. Впиши его в файл конфига com.user.twitchdrones.cfg и перезапусти игру.", "ClientId is not set. Put it into the com.user.twitchdrones.cfg config file and restart the game.")); return; } if (authRunning) { Notify(T("Авторизация уже идёт, заверши её в браузере.", "Authorization is already in progress, finish it in your browser.")); return; } authRunning = true; Task.Run(() => AuthAsync()); } private async Task AuthAsync() { try { int status; string resp = PostForm("https://id.twitch.tv/oauth2/device", "client_id=" + Uri.EscapeDataString(clientId) + "&scopes=" + Uri.EscapeDataString("channel:read:redemptions channel:manage:redemptions user:read:chat moderator:read:chatters"), out status); if (status != 200) { Notify(T("Twitch не принял запрос авторизации. Проверь ClientId, а в настройках приложения на dev.twitch.tv тип клиента должен быть Public. Ответ: ", "Twitch rejected the authorization request. Check the ClientId, and make sure the app client type on dev.twitch.tv is Public. Response: ") + resp); return; } string deviceCode = JsonString(resp, "device_code"); string userCode = JsonString(resp, "user_code"); string verifyUri = JsonString(resp, "verification_uri"); int interval = JsonInt(resp, "interval", 5); int expires = JsonInt(resp, "expires_in", 600); mainThread.Enqueue(delegate { Application.OpenURL(verifyUri); }); Notify(T("Открылась страница Twitch. Нажми там «Авторизовать». Если страница не открылась, зайди на twitch.tv/activate и введи код ", "The Twitch page has opened. Click Authorize there. If the page did not open, go to twitch.tv/activate and enter the code ") + userCode); DateTime deadline = DateTime.UtcNow.AddSeconds(expires); while (DateTime.UtcNow < deadline) { await Task.Delay(interval * 1000); string tokenResp = PostForm("https://id.twitch.tv/oauth2/token", "client_id=" + Uri.EscapeDataString(clientId) + "&scopes=" + Uri.EscapeDataString("channel:read:redemptions channel:manage:redemptions user:read:chat moderator:read:chatters") + "&device_code=" + Uri.EscapeDataString(deviceCode) + "&grant_type=" + Uri.EscapeDataString("urn:ietf:params:oauth:grant-type:device_code"), out status); if (status == 200) { lock (authLock) { accessToken = JsonString(tokenResp, "access_token"); refreshToken = JsonString(tokenResp, "refresh_token"); broadcasterId = null; SaveAuth(); } Notify(T("Twitch подключён. Теперь можно включить интеграцию в настройках мода.", "Twitch connected. You can now enable the integration in the mod settings.")); mainThread.Enqueue(delegate { if (enabledEntry.Value) { StartTwitch(); } }); return; } string message = JsonString(tokenResp, "message"); if (!(message == "authorization_pending")) { if (!(message == "slow_down")) { Notify(T("Авторизация не удалась: ", "Authorization failed: ") + message); return; } interval += 5; } } Notify(T("Время на авторизацию вышло, нажми кнопку ещё раз.", "Authorization time ran out, press the button again.")); } catch (Exception ex) { Exception e = ex; Notify(T("Ошибка авторизации: ", "Authorization error: ") + e.Message); } finally { authRunning = false; } } private bool EnsureToken() { lock (authLock) { if (accessToken != "" && ValidateToken()) { return true; } if (refreshToken != "" && RefreshAccessToken()) { return true; } return false; } } private bool ValidateToken() { try { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create("https://id.twitch.tv/oauth2/validate"); httpWebRequest.Timeout = 10000; httpWebRequest.Headers.Add("Authorization", "OAuth " + accessToken); using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()); string text = streamReader.ReadToEnd(); return text.Contains(clientId) && HasAllScopes(text); } catch (WebException ex) { if (ex.Response == null) { throw; } return false; } } private bool RefreshAccessToken() { int status; string json = PostForm("https://id.twitch.tv/oauth2/token", "client_id=" + Uri.EscapeDataString(clientId) + "&grant_type=refresh_token&refresh_token=" + Uri.EscapeDataString(refreshToken), out status); if (status != 200) { if (status == 400 || status == 401) { accessToken = ""; refreshToken = ""; SaveAuth(); } return false; } if (!HasAllScopes(json)) { accessToken = ""; refreshToken = ""; SaveAuth(); return false; } accessToken = JsonString(json, "access_token"); refreshToken = JsonString(json, "refresh_token"); SaveAuth(); return true; } private async Task RunAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { if (!EnsureToken()) { Notify(T("Twitch: нужна авторизация (или обновление прав после обновления мода). Открой настройки мода и нажми «Авторизоваться в Twitch».", "Twitch: authorization is needed (or the permissions were updated with the mod). Open the mod settings and press Authorize with Twitch.")); break; } await ListenAsync(ct); } catch (OperationCanceledException) { break; } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)(T("Ошибка Twitch: ", "Twitch error: ") + ex2.Message)); } try { await Task.Delay(15000, ct); } catch (OperationCanceledException) { break; } } } private async Task ListenAsync(CancellationToken ct) { using ClientWebSocket ws = new ClientWebSocket(); await ws.ConnectAsync(new Uri("wss://eventsub.wss.twitch.tv/ws"), ct); ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Соединение с Twitch EventSub установлено", "Connected to Twitch EventSub")); byte[] buffer = new byte[8192]; while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested) { using MemoryStream ms = new MemoryStream(); WebSocketReceiveResult result; do { result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), ct); if (result.MessageType == WebSocketMessageType.Close) { return; } ms.Write(buffer, 0, result.Count); } while (!result.EndOfMessage); string text = Encoding.UTF8.GetString(ms.ToArray()); if (HandleMessage(text)) { return; } } } private bool HandleMessage(string text) { Match match = Regex.Match(text, "\"message_type\"\\s*:\\s*\"([^\"]+)\""); if (!match.Success) { return false; } switch (match.Groups[1].Value) { case "session_welcome": { Match match2 = Regex.Match(text, "\"session\"\\s*:\\s*\\{\\s*\"id\"\\s*:\\s*\"([^\"]+)\""); if (match2.Success) { Subscribe(match2.Groups[1].Value); } return false; } case "notification": HandleNotification(text); return false; case "session_reconnect": ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Twitch просит переподключиться", "Twitch asks to reconnect")); return true; case "revocation": ((BaseUnityPlugin)this).Logger.LogWarning((object)T("Twitch отозвал подписку, переподключаюсь", "Twitch revoked the subscription, reconnecting")); return true; default: return false; } } private void HandleNotification(string text) { Match match = Regex.Match(text, "\"subscription_type\"\\s*:\\s*\"([^\"]+)\""); string text2 = (match.Success ? match.Groups[1].Value : ""); if (text2 == "channel.chat.message") { HandleChatMessage(text); } else { notifications.Enqueue(text); } } private void HandleChatMessage(string text) { Match match = Regex.Match(text, "\"chatter_user_id\"\\s*:\\s*\"(\\d+)\""); if (!match.Success) { return; } StringBuilder stringBuilder = new StringBuilder(); bool flag = false; foreach (Match item in Regex.Matches(text, "\"type\"\\s*:\\s*\"(text|mention|emote|cheermote)\"\\s*,\\s*\"text\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")) { flag = true; string value = item.Groups[1].Value; if (!(value == "emote") && !(value == "cheermote")) { stringBuilder.Append(Regex.Unescape(item.Groups[2].Value)); } } if (!flag) { Match match3 = Regex.Match(text, "\"message\"\\s*:\\s*\\{\\s*\"text\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); if (match3.Success) { stringBuilder.Append(Regex.Unescape(match3.Groups[1].Value)); } } string text2 = Regex.Replace(stringBuilder.ToString(), "\\s+", " ").Trim(); if (!(text2 == "") && (!ignoreCommandsEntry.Value || !text2.StartsWith("!"))) { ChatLine chatLine = new ChatLine(); chatLine.UserId = match.Groups[1].Value; chatLine.Text = text2; chatLines.Enqueue(chatLine); } } private void Subscribe(string sessionId) { if (broadcasterId == null) { broadcasterId = GetBroadcasterId(); } EnsureRewards(); string body = "{\"type\":\"channel.channel_points_custom_reward_redemption.add\",\"version\":\"1\",\"condition\":{\"broadcaster_user_id\":\"" + broadcasterId + "\"},\"transport\":{\"method\":\"websocket\",\"session_id\":\"" + sessionId + "\"}}"; HttpRequest("POST", "https://api.twitch.tv/helix/eventsub/subscriptions", body); ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Подписка на награды оформлена, жду покупки...", "Subscribed to redemptions, waiting for purchases...")); try { string body2 = "{\"type\":\"channel.chat.message\",\"version\":\"1\",\"condition\":{\"broadcaster_user_id\":\"" + broadcasterId + "\",\"user_id\":\"" + broadcasterId + "\"},\"transport\":{\"method\":\"websocket\",\"session_id\":\"" + sessionId + "\"}}"; HttpRequest("POST", "https://api.twitch.tv/helix/eventsub/subscriptions", body2); ((BaseUnityPlugin)this).Logger.LogInfo((object)T("Подписка на сообщения чата оформлена", "Subscribed to chat messages")); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось подписаться на сообщения чата: ", "Failed to subscribe to chat messages: ") + ex.Message)); } } private static Dictionary<string, string> ParseRewardList(string json) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); foreach (Match item in Regex.Matches(json, "\"id\"\\s*:\\s*\"([0-9a-fA-F\\-]{36})\"[\\s\\S]*?\"title\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")) { dictionary[Regex.Unescape(item.Groups[2].Value).ToLowerInvariant()] = item.Groups[1].Value; } return dictionary; } private void EnsureRewards() { if (rewardsEnsured) { return; } string json; try { json = HttpRequest("GET", "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + broadcasterId, null); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось получить список наград: ", "Failed to get the reward list: ") + ex.Message)); return; } Dictionary<string, string> dictionary = ParseRewardList(json); foreach (DroneReward reward in rewards) { try { if (dictionary.TryGetValue(reward.Name.ToLowerInvariant(), out var value)) { rewardIds[reward.Name] = value; ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Награда '", "Reward '") + reward.Name + T("' уже есть на канале", "' already exists on the channel"))); continue; } string body = "{\"title\":\"" + JsonEscape(reward.Name) + "\",\"cost\":" + reward.Cost + "}"; string input = HttpRequest("POST", "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + broadcasterId, body); Match match = Regex.Match(input, "\"id\"\\s*:\\s*\"([0-9a-fA-F\\-]{36})\""); if (match.Success) { rewardIds[reward.Name] = match.Groups[1].Value; } ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Награда '", "Reward '") + reward.Name + T("' создана на твоём канале", "' created on your channel"))); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось создать награду '", "Failed to create reward '") + reward.Name + "': " + ex2.Message)); } } rewardsEnsured = true; } private void UpdatePause() { if (pauseFailed) { pauseFailed = false; pauseRetryAt = Time.unscaledTime + 10f; } if (twitchCts == null || pauseBusy || rewardIds.Count == 0 || Time.unscaledTime < pauseRetryAt) { return; } int want = ((pauseOutsideRunEntry.Value && (Object)(object)Run.instance == (Object)null) ? 1 : 0); if (pauseState == want) { return; } pauseBusy = true; Task.Run(delegate { try { SetRewardsPaused(want == 1); pauseState = want; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось изменить паузу наград: ", "Failed to change the reward pause: ") + ex.Message)); pauseFailed = true; } finally { pauseBusy = false; } }); } private void SetRewardsPaused(bool paused) { foreach (string value in rewardIds.Values) { try { HttpRequest("PATCH", "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + broadcasterId + "&id=" + value, "{\"is_paused\":" + (paused ? "true" : "false") + "}"); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось поставить паузу награде: ", "Failed to pause a reward: ") + ex.Message)); } } ((BaseUnityPlugin)this).Logger.LogInfo((object)(paused ? T("Награды на паузе (ты не в забеге)", "Rewards paused (you are not in a run)") : T("Награды снова активны", "Rewards active again"))); } private void SetRedemptionStatus(string redemptionId, string rewardId, string status) { if (string.IsNullOrEmpty(redemptionId) || string.IsNullOrEmpty(rewardId)) { return; } Task.Run(delegate { try { HttpRequest("PATCH", "https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?id=" + redemptionId + "&broadcaster_id=" + broadcasterId + "&reward_id=" + rewardId, "{\"status\":\"" + status + "\"}"); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось закрыть покупку (", "Failed to close the redemption (") + status + "): " + ex.Message)); } }); } private void DeleteRewards(bool showMessage) { try { if (!EnsureToken()) { return; } if (broadcasterId == null) { broadcasterId = GetBroadcasterId(); } string input = HttpRequest("GET", "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + broadcasterId, null); int num = 0; foreach (Match item in Regex.Matches(input, "\"id\"\\s*:\\s*\"([0-9a-fA-F\\-]{36})\"[\\s\\S]*?\"title\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")) { string value = item.Groups[1].Value; string text = Regex.Unescape(item.Groups[2].Value); if (FindReward(text) != null) { try { HttpRequest("DELETE", "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + broadcasterId + "&id=" + value, null); num++; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось удалить награду '", "Failed to delete reward '") + text + "': " + ex.Message)); } } } if (showMessage) { Notify(T("Интеграция выключена, наград удалено с канала: ", "Integration disabled, rewards removed from the channel: ") + num); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)(T("Выход из игры, наград удалено с канала: ", "Game exit, rewards removed from the channel: ") + num)); } } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(T("Не удалось удалить награды: ", "Failed to delete rewards: ") + ex2.Message)); } } private static bool HasAllScopes(string json) { string[] array = "channel:read:redemptions channel:manage:redemptions user:read:chat moderator:read:chatters".Split(new char[1] { ' ' }); foreach (string value in array) { if (!json.Contains(value)) { return false; } } return true; } private static string JsonEscape(string text) { return text.Replace("\\", "\\\\").Replace("\"", "\\\""); } private static string JsonString(string json, string key) { Match match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); return match.Success ? Regex.Unescape(match.Groups[1].Value) : ""; } private static int JsonInt(string json, string key, int fallback) { Match match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(\\d+)"); if (match.Success && int.TryParse(match.Groups[1].Value, out var result)) { return result; } return fallback; } private string GetBroadcasterId() { string text = HttpRequest("GET", "https://api.twitch.tv/helix/users", null); Match match = Regex.Match(text, "\"id\"\\s*:\\s*\"(\\d+)\""); if (!match.Success) { throw new Exception(T("Не удалось узнать id канала: ", "Failed to get the channel id: ") + text); } return match.Groups[1].Value; } private string HttpRequest(string method, string url, string body) { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url); httpWebRequest.Method = method; httpWebRequest.Timeout = 10000; httpWebRequest.Headers.Add("Authorization", "Bearer " + accessToken); httpWebRequest.Headers.Add("Client-Id", clientId); if (body != null) { httpWebRequest.ContentType = "application/json"; byte[] bytes = Encoding.UTF8.GetBytes(body); httpWebRequest.ContentLength = bytes.Length; using Stream stream = httpWebRequest.GetRequestStream(); stream.Write(bytes, 0, bytes.Length); } try { using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()); return streamReader.ReadToEnd(); } catch (WebException ex) { string text = ex.Message; if (ex.Response != null) { using StreamReader streamReader2 = new StreamReader(ex.Response.GetResponseStream()); text = streamReader2.ReadToEnd(); } throw new Exception("Twitch API: " + text); } } private string PostForm(string url, string form, out int status) { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url); httpWebRequest.Method = "POST"; httpWebRequest.Timeout = 15000; httpWebRequest.ContentType = "application/x-www-form-urlencoded"; byte[] bytes = Encoding.UTF8.GetBytes(form); httpWebRequest.ContentLength = bytes.Length; using (Stream stream = httpWebRequest.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } try { using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()); status = (int)httpWebResponse.StatusCode; return streamReader.ReadToEnd(); } catch (WebException ex) { if (!(ex.Response is HttpWebResponse httpWebResponse2)) { throw; } status = (int)httpWebResponse2.StatusCode; using StreamReader streamReader2 = new StreamReader(httpWebResponse2.GetResponseStream()); return streamReader2.ReadToEnd(); } } }