├── .gitignore ├── HDT-TwitchPlugin.sln ├── README.md └── TwitchPlugin ├── ChatCommands.cs ├── CommandInformation.xaml ├── CommandInformation.xaml.cs ├── Config.cs ├── Core.cs ├── IRC.cs ├── Properties └── AssemblyInfo.cs ├── SettingsWindow.xaml ├── SettingsWindow.xaml.cs ├── TwitchChatMessage.cs ├── TwitchPlugin.cs ├── TwitchPlugin.csproj └── UpdateCheck.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | HDT-TwitchPlugin.sln.ide/edb.chk 198 | HDT-TwitchPlugin.sln.ide/edbres00001.jrs 199 | HDT-TwitchPlugin.sln.ide/edbres00002.jrs 200 | HDT-TwitchPlugin.sln.ide/storage.ide 201 | -------------------------------------------------------------------------------- /HDT-TwitchPlugin.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchPlugin", "TwitchPlugin\TwitchPlugin.csproj", "{3B25311E-167E-4E36-A7C3-81C92B33DF10}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x86 = Debug|x86 12 | Release|Any CPU = Release|Any CPU 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Debug|x86.ActiveCfg = Debug|x86 19 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Debug|x86.Build.0 = Debug|x86 20 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Release|x86.ActiveCfg = Release|x86 23 | {3B25311E-167E-4E36-A7C3-81C92B33DF10}.Release|x86.Build.0 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HDT-TwitchPlugin 2 | This is a plugin for the [Hearthstone Deck Tracker](https://github.com/HearthSim/Hearthstone-Deck-Tracker). 3 | It logs into a twitch.tv account you provide and responds to chat commands in your channel (listed below). 4 | 5 | ![commands](http://i.imgur.com/8Jaslz8.png) 6 | 7 | [List of all commands](https://github.com/azeier/HDT-TwitchPlugin/wiki/Commands) 8 | 9 | # Requirements: 10 | - Latest version of [HDT](https://github.com/HearthSim/Hearthstone-Deck-Tracker) 11 | - You are logged in to your HearthStats account in HDT (`PLUGINS > HEARTHSTATS`) 12 | - HDT is synced with HearthStats, `AUTO UPLOAD NEW GAMES` is enabled 13 | 14 | # Instructions 15 | 1. Place `TwitchPlugin.dll` in `Hearthstone Deck Tracker/Plugins` 16 | - Restart/Start HDT 17 | - Enable plugin via `options > Tracker > Plugins` 18 | - In options: click `SETTINGS` with the plugin selected OR in the menu bar via `PLUGINS > TWITCH > SETTINGS` 19 | - Enter connection settings: 20 | 1. username: the twitch.tv account HDT will be using in chat (can be your own or you can create a new one) 21 | - oauth key: the "password" used to connect to IRC. If you don't have one yet, click the link below the textbox. (Login with the Twitch account you want HDT to use!) 22 | - channel: name of your twitch channel ("twitch.tv/epix37" -> "epix37") 23 | - Click the `INFO` button in the bottom left to see what the commands do. 24 | - `PLUGINS > TWITCH > CONNECT` 25 | - HDT will say "Hi!" in chat to let you know it's connected. 26 | 27 | # Other 28 | Commands can currently be executed once every ten seconds. 29 | 30 | # Download: 31 | Get the latest version here: https://github.com/azeier/HDT-TwitchPlugin/releases 32 | 33 | # Contact 34 | If you have questions, suggestions or just want to talk feel free to email me! 35 | Email: alex@hearthsim.net 36 | -------------------------------------------------------------------------------- /TwitchPlugin/ChatCommands.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Hearthstone_Deck_Tracker; 7 | using Hearthstone_Deck_Tracker.Enums; 8 | using Hearthstone_Deck_Tracker.Hearthstone; 9 | using Hearthstone_Deck_Tracker.Stats; 10 | using Hearthstone_Deck_Tracker.Utility.Logging; 11 | 12 | #endregion 13 | 14 | namespace TwitchPlugin 15 | { 16 | public class ChatCommands 17 | { 18 | private const string HssUrl = "http://hss.io/d/"; 19 | 20 | private const string MissingTimeFrameMessage = 21 | "Please specify a timeframe. Available timeframes are: today, week, season and total. (Example: !{0} today)"; 22 | private static int _winStreak; 23 | private static GameStats _lastGame; 24 | private static readonly string[] KillingSprees = {"Killing Spree", "Rampage", "Dominating", "Unstoppable", "GODLIKE", "WICKED SICK"}; 25 | 26 | public static void AllDecksCommand() 27 | { 28 | var decks = DeckList.Instance.Decks.Where(d => d.Tags.Contains(Core.TwitchTag)).ToList(); 29 | if(!decks.Any()) 30 | return; 31 | var response = 32 | decks.Select(d => $"{d.Name.Replace(" ", "_")}:{HssUrl + d.HearthStatsId}").Aggregate((c, n) => c + ", " + n); 33 | Core.Send(response); 34 | } 35 | 36 | public static void DeckCommand() 37 | { 38 | var deck = DeckList.Instance.ActiveDeckVersion; 39 | if(deck == null) 40 | { 41 | Core.Send("No active deck."); 42 | return; 43 | } 44 | if(deck.IsArenaDeck) 45 | Core.Send($"Current arena run ({deck.Class}): {deck.WinLossString}, DeckList: {"[currently only supported for constructed decks]"}"); 46 | else 47 | Core.Send($"Currently using \"{deck.Name}\", Winrate: {deck.WinPercentString} ({deck.WinLossString}), Decklist: {HssUrl + deck.HearthStatsId}"); 48 | } 49 | 50 | public static void StatsCommand(string arg) 51 | { 52 | if(string.IsNullOrEmpty(arg)) 53 | { 54 | Core.Send(string.Format(MissingTimeFrameMessage, "stats")); 55 | return; 56 | } 57 | var games = DeckStatsList.Instance.DeckStats.SelectMany(ds => ds.Value.Games).Where(TimeFrameFilter(arg)).ToList(); 58 | var numGames = games.Count; 59 | var timeFrame = arg == "today" || arg == "total" ? arg : "this " + arg; 60 | if(numGames == 0) 61 | { 62 | Core.Send($"No games played {timeFrame}."); 63 | return; 64 | } 65 | var numDecks = games.Select(g => g.DeckId).Distinct().Count(); 66 | var wins = games.Count(g => g.Result == GameResult.Win); 67 | var winRate = Math.Round(100.0 * wins / numGames); 68 | Core.Send($"Played {numGames} games with {numDecks} decks {timeFrame}. Total stats: {wins}-{numGames - wins} ({winRate}%)"); 69 | } 70 | 71 | public static void ArenaCommand(string arg) 72 | { 73 | if(string.IsNullOrEmpty(arg)) 74 | { 75 | Core.Send(string.Format(MissingTimeFrameMessage, "arena")); 76 | return; 77 | } 78 | var arenaRuns = DeckList.Instance.Decks.Where(d => d.IsArenaDeck).ToList(); 79 | switch(arg) 80 | { 81 | case "today": 82 | arenaRuns = arenaRuns.Where(g => g.LastPlayed.Date == DateTime.Today).ToList(); 83 | break; 84 | case "week": 85 | arenaRuns = arenaRuns.Where(g => g.LastPlayed.Date > DateTime.Today.AddDays(-7)).ToList(); 86 | break; 87 | case "season": 88 | arenaRuns = 89 | arenaRuns.Where(g => g.LastPlayed.Date.Year == DateTime.Today.Year && g.LastPlayed.Date.Month == DateTime.Today.Month).ToList(); 90 | break; 91 | case "total": 92 | break; 93 | default: 94 | return; 95 | } 96 | var timeFrame = arg == "today" || arg == "total" ? arg : "this " + arg; 97 | if(!arenaRuns.Any()) 98 | { 99 | Core.Send($"No arena runs {timeFrame}."); 100 | return; 101 | } 102 | var ordered = 103 | arenaRuns.Select(run => new {Run = run, Wins = run.DeckStats.Games.Count(g => g.Result == GameResult.Win)}) 104 | .OrderByDescending(x => x.Wins); 105 | var best = ordered.Where(run => run.Wins == ordered.First().Wins).ToList(); 106 | var classesObj = best.Select(x => x.Run.Class).Distinct().Select(x => new {Class = x, Count = best.Count(c => c.Run.Class == x)}); 107 | var classes = 108 | classesObj.Select(x => x.Class + (x.Count > 1 ? $" (x{x.Count})" : "")).Aggregate((c, n) => c + ", " + n); 109 | Core.Send($"Best arena run {timeFrame}: {best.First().Run.WinLossString} with {classes}"); 110 | } 111 | 112 | public static void BestDeckCommand(string arg) 113 | { 114 | if(string.IsNullOrEmpty(arg)) 115 | { 116 | Core.Send(string.Format(MissingTimeFrameMessage, "bestdeck")); 117 | return; 118 | } 119 | var decks = 120 | DeckList.Instance.Decks.Where(d => !d.IsArenaDeck) 121 | .Select(d => new {Deck = d, Games = d.DeckStats.Games.Where(TimeFrameFilter(arg))}); 122 | var stats = 123 | decks.Select( 124 | d => 125 | new 126 | { 127 | DeckObj = d, 128 | Wins = d.Games.Count(g => g.Result == GameResult.Win), 129 | Losses = (d.Games.Count(g => g.Result == GameResult.Loss)) 130 | }) 131 | .Where(d => d.Wins + d.Losses > Config.Instance.BestDeckGamesThreshold) 132 | .OrderByDescending(d => (double)d.Wins / (d.Wins + d.Losses)); 133 | var best = stats.FirstOrDefault(); 134 | var timeFrame = arg == "today" || arg == "total" ? arg : "this " + arg; 135 | if(best == null) 136 | { 137 | if(Config.Instance.BestDeckGamesThreshold > 1) 138 | Core.Send($"Not enough games played {timeFrame} (min: {Config.Instance.BestDeckGamesThreshold})"); 139 | else 140 | Core.Send("No games played " + timeFrame); 141 | return; 142 | } 143 | var winRate = Math.Round(100.0 * best.Wins / (best.Wins + best.Losses), 0); 144 | Core.Send($"Best deck {timeFrame}: \"{best.DeckObj.Deck.Name}\", Winrate: {winRate}% ({best.Wins}-{best.Losses}), Decklist: {HssUrl + best.DeckObj.Deck.HearthStatsId}"); 145 | } 146 | 147 | public static void MostPlayedCommand(string arg) 148 | { 149 | if(string.IsNullOrEmpty(arg)) 150 | { 151 | Core.Send(string.Format(MissingTimeFrameMessage, "mostplayed")); 152 | return; 153 | } 154 | var decks = 155 | DeckList.Instance.Decks.Where(d => !d.IsArenaDeck) 156 | .Select(d => new {Deck = d, Games = d.DeckStats.Games.Where(TimeFrameFilter(arg))}); 157 | var mostPlayed = decks.Where(d => d.Games.Any()).OrderByDescending(d => d.Games.Count()).FirstOrDefault(); 158 | var timeFrame = arg == "today" || arg == "total" ? arg : "this " + arg; 159 | if(mostPlayed == null) 160 | { 161 | Core.Send("No games played " + timeFrame); 162 | return; 163 | } 164 | var wins = mostPlayed.Games.Count(g => g.Result == GameResult.Win); 165 | var losses = mostPlayed.Games.Count(g => g.Result == GameResult.Loss); 166 | var winRate = Math.Round(100.0 * wins / (wins + losses), 0); 167 | Core.Send($"Most played deck {timeFrame}: \"{mostPlayed.Deck.Name}\", Winrate: {winRate}% ({wins}-{losses}), Decklist: {HssUrl + mostPlayed.Deck.HearthStatsId}"); 168 | } 169 | 170 | public static Func TimeFrameFilter(string timeFrame) 171 | { 172 | switch(timeFrame) 173 | { 174 | case "today": 175 | return game => game.StartTime.Date == DateTime.Today; 176 | case "week": 177 | return game => game.StartTime > DateTime.Today.AddDays(-7); 178 | case "season": 179 | return game => game.StartTime.Date.Year == DateTime.Today.Year && game.StartTime.Date.Month == DateTime.Today.Month; 180 | case "total": 181 | return game => true; 182 | default: 183 | return game => false; 184 | } 185 | } 186 | 187 | public static void HdtCommand() => Core.Send("Hearthstone Deck Tracker: http://hsdecktracker.net"); 188 | 189 | public static void OnGameEnd() 190 | { 191 | _lastGame = Hearthstone_Deck_Tracker.API.Core.Game.CurrentGameStats.CloneWithNewId(); 192 | if(_lastGame.Result == GameResult.Win) 193 | _winStreak++; 194 | else 195 | _winStreak = 0; 196 | } 197 | 198 | public static async void OnInMenu() 199 | { 200 | if(!Config.Instance.AutoPostGameResult) 201 | return; 202 | if(_lastGame == null) 203 | return; 204 | var winStreak = _winStreak > 2 205 | ? $"{GetKillingSpree(_winStreak)}! {GetOrdinal(_winStreak)} win in a row" 206 | : _lastGame.Result.ToString(); 207 | var deck = DeckList.Instance.ActiveDeckVersion; 208 | var winLossString = deck != null ? ": " + deck.WinLossString : ""; 209 | var message = 210 | $"{winStreak} vs {_lastGame.OpponentName} ({_lastGame.OpponentHero.ToLower()}) after {_lastGame.Duration}{winLossString}"; 211 | _lastGame = null; 212 | if(Config.Instance.AutoPostDelay > 0) 213 | { 214 | Log.Info($"Waiting {Config.Instance.AutoPostDelay} seconds before posting game result...", "TwitchPlugin"); 215 | await Task.Delay(Config.Instance.AutoPostDelay * 1000); 216 | } 217 | Core.Send(message); 218 | } 219 | 220 | private static string GetKillingSpree(int wins) 221 | { 222 | var index = wins / 3 - 1; 223 | if(index < 0) 224 | return ""; 225 | if(index > 5) 226 | index = 5; 227 | return KillingSprees[index]; 228 | } 229 | 230 | //http://www.c-sharpcorner.com/UploadFile/b942f9/converting-cardinal-numbers-to-ordinal-using-C-Sharp/ 231 | private static string GetOrdinal(int number) 232 | { 233 | if(number < 0) 234 | return number.ToString(); 235 | var rem = number % 100; 236 | if(rem >= 11 && rem <= 13) 237 | return number + "th"; 238 | switch(number % 10) 239 | { 240 | case 1: 241 | return number + "st"; 242 | case 2: 243 | return number + "nd"; 244 | case 3: 245 | return number + "rd"; 246 | default: 247 | return number + "th"; 248 | } 249 | } 250 | 251 | public static void CommandsCommand() => Core.Send("List of available commands: https://github.com/azeier/HDT-TwitchPlugin/wiki/Commands"); 252 | } 253 | } -------------------------------------------------------------------------------- /TwitchPlugin/CommandInformation.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | All commands linking decks require HDT to be synced with HearthStats. 12 | 13 | 14 | 15 | 16 | 17 | Returns a list of enabled commands. 18 | 19 | 20 | 21 | 22 | Returns information about the currently active (selected) deck. 23 | 24 | Arena: 25 | Current arena run ({class}): {win-loss}, DeckList: {url}[not yet implemented] 26 | 27 | Constructed: 28 | Currently using "{deckname}", Winrate: {win%} ({win-loss}), Decklist: {url} 29 | 30 | 31 | 32 | 33 | Returns a list of all decks tagged with "TwitchPlugin": 34 | 35 | {deckname1}: {url1}, {deckname2}: {url2}, ... 36 | 37 | To add a deck to this list right-click a deck in HDT and add the "TwitchPlugin" tag via "QUICK SET TAG" 38 | 39 | 40 | 41 | 42 | Returns a downloadlink to HDT. 43 | 44 | 45 | 46 | 47 | Returns information about your stats for the given timeframe: 48 | 49 | Played {#games} games with {#used decks} decks. Total stats: {win-loss} 50 | 51 | timeframes: 52 | today, week, season, total 53 | 54 | 55 | 56 | 57 | Returns information about your best arena run for the given timeframe: 58 | 59 | Best arena run {timeframe}: {win-loss} with {classes} 60 | 61 | timeframes: 62 | today, week, season, total 63 | 64 | 65 | 66 | 67 | Returns your best constructed deck (highest winrate) for the given timeframe: 68 | 69 | Best deck {timeframe}: "{deckname}", Winrate: {win%} ({win-loss}), Decklist: {url} 70 | 71 | timeframes: 72 | today, week, season, total 73 | 74 | 75 | 76 | 77 | Returns your most played deck (highest number of games) for the given timeframe: 78 | 79 | Most played deck {timeframe}: "{deckname}", Winrate: {win%} ({win-loss}), Decklist: {url} 80 | 81 | timeframes: 82 | today, week, season, total 83 | 84 | 85 | 86 | 87 | 88 | 89 | Writes details about the match, after leaving the victory/defeat screen: 90 | 91 | {result} VS {opponentname} ({opponent class}) after {x min}: {deck win-loss} 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /TwitchPlugin/CommandInformation.xaml.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Windows.Controls; 4 | 5 | #endregion 6 | 7 | namespace TwitchPlugin 8 | { 9 | /// 10 | /// Interaction logic for CommandInformation.xaml 11 | /// 12 | public partial class CommandInformation : UserControl 13 | { 14 | public CommandInformation() 15 | { 16 | InitializeComponent(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /TwitchPlugin/Config.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.IO; 4 | using Hearthstone_Deck_Tracker; 5 | 6 | #endregion 7 | 8 | namespace TwitchPlugin 9 | { 10 | public class Config 11 | { 12 | private static Config _instance; 13 | 14 | public static string[] TimeFrames => new[] {"today", "week", "season", "total"}; 15 | 16 | public Config() 17 | { 18 | AutoPostGameResult = true; 19 | AutoPostDelay = 0; 20 | ChatCommandCommands = true; 21 | ChatCommandDeck = true; 22 | ChatCommandAllDecks = true; 23 | ChatCommandHdt = true; 24 | ChatCommandStatsGeneral = true; 25 | ChatCommandStatsDefault = "today"; 26 | ChatCommandStatsToday = true; 27 | ChatCommandStatsWeek = true; 28 | ChatCommandStatsSeason = true; 29 | ChatCommandStatsTotal = true; 30 | ChatCommandArenaGeneral = true; 31 | ChatCommandArenaDefault = "today"; 32 | ChatCommandArenaToday = true; 33 | ChatCommandArenaWeek = true; 34 | ChatCommandArenaSeason = true; 35 | ChatCommandArenaTotal = true; 36 | ChatCommandBestDeckGeneral = true; 37 | ChatCommandBestDeckDefault = "today"; 38 | ChatCommandBestDeckToday = true; 39 | ChatCommandBestDeckWeek = true; 40 | ChatCommandBestDeckSeason = true; 41 | ChatCommandBestDeckTotal = true; 42 | ChatCommandMostPlayedGeneral = true; 43 | ChatCommandMostPlayedDefault = "today"; 44 | ChatCommandMostPlayedToday = true; 45 | ChatCommandMostPlayedWeek = true; 46 | ChatCommandMostPlayedSeason = true; 47 | ChatCommandMostPlayedTotal = true; 48 | BestDeckGamesThreshold = 3; 49 | StatsFileName = "hdt_activedeck_stats.txt"; 50 | StatsFileDir = Hearthstone_Deck_Tracker.Config.Instance.DataDir; 51 | } 52 | 53 | public static Config Instance => _instance ?? Load(); 54 | 55 | public string User { get; set; } 56 | public string OAuth { get; set; } 57 | public string Channel { get; set; } 58 | public bool AutoPostGameResult { get; set; } 59 | public int AutoPostDelay { get; set; } 60 | public bool ChatCommandCommands { get; set; } 61 | public bool ChatCommandDeck { get; set; } 62 | public bool ChatCommandAllDecks { get; set; } 63 | public bool ChatCommandHdt { get; set; } 64 | public bool ChatCommandStatsGeneral { get; set; } 65 | public string ChatCommandStatsDefault { get; set; } 66 | public bool ChatCommandStatsToday { get; set; } 67 | public bool ChatCommandStatsWeek { get; set; } 68 | public bool ChatCommandStatsSeason { get; set; } 69 | public bool ChatCommandStatsTotal { get; set; } 70 | public bool ChatCommandArenaGeneral { get; set; } 71 | public string ChatCommandArenaDefault { get; set; } 72 | public bool ChatCommandArenaToday { get; set; } 73 | public bool ChatCommandArenaWeek { get; set; } 74 | public bool ChatCommandArenaSeason { get; set; } 75 | public bool ChatCommandArenaTotal { get; set; } 76 | public bool ChatCommandBestDeckGeneral { get; set; } 77 | public string ChatCommandBestDeckDefault { get; set; } 78 | public bool ChatCommandBestDeckToday { get; set; } 79 | public bool ChatCommandBestDeckWeek { get; set; } 80 | public bool ChatCommandBestDeckSeason { get; set; } 81 | public bool ChatCommandBestDeckTotal { get; set; } 82 | public bool ChatCommandMostPlayedGeneral { get; set; } 83 | public string ChatCommandMostPlayedDefault { get; set; } 84 | public bool ChatCommandMostPlayedToday { get; set; } 85 | public bool ChatCommandMostPlayedWeek { get; set; } 86 | public bool ChatCommandMostPlayedSeason { get; set; } 87 | public bool ChatCommandMostPlayedTotal { get; set; } 88 | public int BestDeckGamesThreshold { get; set; } 89 | public bool SaveStatsToFile { get; set; } 90 | public string StatsFileDir { get; set; } 91 | public string StatsFileName { get; set; } 92 | public string StatsFileFullPath => Path.Combine(StatsFileDir, StatsFileName); 93 | public bool IrcLogging { get; set; } 94 | 95 | private static string FilePath => Path.Combine(Hearthstone_Deck_Tracker.Config.Instance.ConfigDir, "TwitchPlugin.xml"); 96 | 97 | public static T GetConfigItem(string name) 98 | { 99 | var prop = Instance.GetType().GetProperty(name).GetValue(Instance, null); 100 | if(prop == null) 101 | return default(T); 102 | return (T)prop; 103 | } 104 | 105 | public static void Save() => XmlManager.Save(FilePath, Instance); 106 | 107 | private static Config Load() => _instance = File.Exists(FilePath) ? XmlManager.Load(FilePath) : new Config(); 108 | } 109 | } -------------------------------------------------------------------------------- /TwitchPlugin/Core.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using Hearthstone_Deck_Tracker; 9 | using Hearthstone_Deck_Tracker.Enums; 10 | using Hearthstone_Deck_Tracker.Utility.Logging; 11 | using static TwitchPlugin.ChatCommands; 12 | 13 | #endregion 14 | 15 | namespace TwitchPlugin 16 | { 17 | public class Core 18 | { 19 | private static IRC _irc; 20 | private static readonly Dictionary Commands; 21 | 22 | static Core() 23 | { 24 | Commands = new Dictionary(); 25 | AddCommand("commands", CommandsCommand, "ChatCommandCommands"); 26 | AddCommand("deck", DeckCommand, "ChatCommandDeck"); 27 | AddCommand("alldecks", AllDecksCommand, "ChatCommandAllDecks"); 28 | AddCommand("hdt", HdtCommand, "ChatCommandHdt"); 29 | AddCommand("stats", () => StatsCommand(Config.Instance.ChatCommandStatsDefault), "ChatCommandStatsGeneral"); 30 | AddCommand("stats today", () => StatsCommand("today"), "ChatCommandStatsToday", "ChatCommandStatsGeneral"); 31 | AddCommand("stats week", () => StatsCommand("week"), "ChatCommandStatsWeek", "ChatCommandStatsGeneral"); 32 | AddCommand("stats season", () => StatsCommand("season"), "ChatCommandStatsSeason", "ChatCommandStatsGeneral"); 33 | AddCommand("stats total", () => StatsCommand("total"), "ChatCommandStatsTotal", "ChatCommandStatsGeneral"); 34 | AddCommand("arena", () => ArenaCommand(Config.Instance.ChatCommandArenaDefault), "ChatCommandArenaGeneral"); 35 | AddCommand("arena today", () => ArenaCommand("today"), "ChatCommandArenaToday", "ChatCommandArenaGeneral"); 36 | AddCommand("arena week", () => ArenaCommand("week"), "ChatCommandArenaWeek", "ChatCommandArenaGeneral"); 37 | AddCommand("arena season", () => ArenaCommand("season"), "ChatCommandArenaSeason", "ChatCommandArenaGeneral"); 38 | AddCommand("arena total", () => ArenaCommand("total"), "ChatCommandArenaTotal", "ChatCommandArenaGeneral"); 39 | AddCommand("bestdeck", () => BestDeckCommand(Config.Instance.ChatCommandBestDeckDefault), "ChatCommandBestDeckGeneral"); 40 | AddCommand("bestdeck today", () => BestDeckCommand("today"), "ChatCommandBestDeckToday", "ChatCommandBestDeckGeneral"); 41 | AddCommand("bestdeck week", () => BestDeckCommand("week"), "ChatCommandBestDeckWeek", "ChatCommandBestDeckGeneral"); 42 | AddCommand("bestdeck season", () => BestDeckCommand("season"), "ChatCommandBestDeckSeason", 43 | "ChatCommandBestDeckGeneral"); 44 | AddCommand("bestdeck total", () => BestDeckCommand("total"), "ChatCommandBestDeckTotal", "ChatCommandBestDeckGeneral"); 45 | AddCommand("mostplayed", () => MostPlayedCommand(Config.Instance.ChatCommandMostPlayedDefault), 46 | "ChatCommandMostPlayedGeneral"); 47 | AddCommand("mostplayed today", () => MostPlayedCommand("today"), "ChatCommandMostPlayedToday", 48 | "ChatCommandMostPlayedGeneral"); 49 | AddCommand("mostplayed week", () => MostPlayedCommand("week"), "ChatCommandMostPlayedWeek", 50 | "ChatCommandMostPlayedGeneral"); 51 | AddCommand("mostplayed season", () => MostPlayedCommand("season"), "ChatCommandMostPlayedSeason", 52 | "ChatCommandMostPlayedGeneral"); 53 | AddCommand("mostplayed total", () => MostPlayedCommand("total"), "ChatCommandMostPlayedTotal", 54 | "ChatCommandMostPlayedGeneral"); 55 | } 56 | 57 | public static string TwitchTag => "TwitchPlugin"; 58 | 59 | public static List GetCommandNames() => Commands.Select(x => x.Key).ToList(); 60 | 61 | public static void AddCommand(string command, Action action, string propName, string generalPropName = null) => Commands.Add(command, new ChatCommand(command, action, propName, generalPropName)); 62 | 63 | internal static void Send(string message) 64 | { 65 | if(_irc == null) 66 | return; 67 | _irc.SendMessage(Config.Instance.Channel.ToLower(), message); 68 | Log.Info(message, "TwitchPlugin"); 69 | } 70 | 71 | public static bool Connect() 72 | { 73 | Log.Info("Logging in as " + Config.Instance.User); 74 | _irc = new IRC(Config.Instance.User, Config.Instance.User, Config.Instance.OAuth); 75 | if (!_irc.Connect("irc.twitch.tv", 6667)) 76 | return false; 77 | _irc.JoinChannel(Config.Instance.Channel.ToLower()); 78 | _irc.OnChatMsg += HandleChatMessage; 79 | Send("Hi! (Hearthstone Deck Tracker connected)"); 80 | return true; 81 | } 82 | 83 | private static void HandleChatMessage(TwitchChatMessage msg) 84 | { 85 | if(!msg.Message.StartsWith("!")) 86 | return; 87 | var cmd = msg.Message.Substring(1); 88 | ChatCommand chatCommand; 89 | if(Commands.TryGetValue(cmd, out chatCommand)) 90 | chatCommand.Execute(msg); 91 | else 92 | Log.Info($"Unknown command by {msg.User}: {msg.Message}", "TwitchPlugin"); 93 | } 94 | 95 | public static void Disconnect() 96 | { 97 | if(_irc == null || !_irc.Connected) 98 | return; 99 | Send("Bye! (Hearthstone Deck Tracker disconnected)"); 100 | _irc.LeaveChannel(Config.Instance.Channel.ToLower()); 101 | _irc.Quit(); 102 | } 103 | 104 | private static string _currentFileContent; 105 | public static void Update() 106 | { 107 | if(!Config.Instance.SaveStatsToFile) 108 | return; 109 | if(DeckList.Instance.ActiveDeckVersion == null) 110 | return; 111 | var games = DeckList.Instance.ActiveDeckVersion.GetRelevantGames(); 112 | var wins = games.Count(g => g.Result == GameResult.Win); 113 | var losses = games.Count(g => g.Result == GameResult.Loss); 114 | var resultString = $"{wins} - {losses}"; 115 | if(_currentFileContent == resultString) 116 | return; 117 | try 118 | { 119 | using(var sr = new StreamWriter(Config.Instance.StatsFileFullPath)) 120 | sr.WriteLine(resultString); 121 | _currentFileContent = resultString; 122 | } 123 | catch(Exception ex) 124 | { 125 | //uncomment for v0.11.5 ? 126 | //Hearthstone_Deck_Tracker.API.Errors.ShowErrorMessage("TwitchPlugin", ex.ToString()); 127 | Log.Error("Error writing to stats file: " + ex, "TwitchPlugin"); 128 | } 129 | } 130 | } 131 | 132 | public class ChatCommand 133 | { 134 | private readonly Action _action; 135 | private readonly string _command; 136 | private readonly string _configItem; 137 | private readonly string _generalConfigItem; 138 | private DateTime _lastExecute; 139 | 140 | public ChatCommand(string command, Action action, string configItem, string generalConfigItem = null) 141 | { 142 | _command = command; 143 | _action = action; 144 | _lastExecute = DateTime.MinValue; 145 | _configItem = configItem; 146 | _generalConfigItem = generalConfigItem; 147 | } 148 | 149 | public void Execute(TwitchChatMessage msg) 150 | { 151 | Log.Info($"Command \"{_command}\" requested by {msg.User}.", "TwitchPlugin"); 152 | if(_generalConfigItem != null && !Config.GetConfigItem(_generalConfigItem)) 153 | { 154 | Log.Info($"Command \"{_command}\" is disabled (general).", "TwitchPlugin"); 155 | return; 156 | } 157 | if(!Config.GetConfigItem(_configItem)) 158 | { 159 | Log.Info($"Command \"{_command}\" is disabled.", "TwitchPlugin"); 160 | return; 161 | } 162 | if((DateTime.Now - _lastExecute).TotalSeconds < 10) 163 | { 164 | Log.Info($"Time since last execute of {_command} is less than 10 seconds. Not executing.", "TwitchPlugin"); 165 | return; 166 | } 167 | _lastExecute = DateTime.Now; 168 | _action.Invoke(); 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /TwitchPlugin/IRC.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System; 4 | using System.IO; 5 | using System.Net.Sockets; 6 | using System.Threading; 7 | using Hearthstone_Deck_Tracker; 8 | using Hearthstone_Deck_Tracker.Utility.Logging; 9 | 10 | #endregion 11 | 12 | namespace TwitchPlugin 13 | { 14 | internal class IRC 15 | { 16 | public delegate void ChatMsg(TwitchChatMessage msg); 17 | 18 | public delegate void Receive(string msg); 19 | 20 | public delegate void UserColor(string user, string color); 21 | 22 | private readonly string _name; 23 | private readonly string _nick; 24 | private readonly string _oauth; 25 | private TcpClient _connection; 26 | private NetworkStream _nwStream; 27 | private StreamReader _reader; 28 | private StreamWriter _writer; 29 | 30 | public IRC(string name, string nick, string oauth) 31 | { 32 | _name = name; 33 | _nick = nick; 34 | _oauth = oauth; 35 | } 36 | 37 | public bool Connected => _connection.Connected; 38 | 39 | public event ChatMsg OnChatMsg; 40 | 41 | public bool Connect(string server, int port) 42 | { 43 | try 44 | { 45 | _connection = new TcpClient(server, port); 46 | } 47 | catch(Exception ex) 48 | { 49 | Log.Error("Error connecting: " + ex, "TwitchPlugin"); 50 | return false; 51 | } 52 | 53 | try 54 | { 55 | _nwStream = _connection.GetStream(); 56 | _reader = new StreamReader(_nwStream); 57 | _writer = new StreamWriter(_nwStream); 58 | 59 | var thread = new Thread(Listen) {IsBackground = true}; 60 | thread.Start(); 61 | 62 | _writer.AutoFlush = true; 63 | string oauth = _oauth; 64 | if(!oauth.StartsWith("oauth:")) 65 | oauth = "oauth:" + _oauth; 66 | SendData("PASS", oauth, false); 67 | SendData("NICK", _nick); 68 | SendData("USER", _name + " 8 * :" + _nick); 69 | //SendData("JTVCLIENT"); 70 | } 71 | catch(Exception ex) 72 | { 73 | Log.Error("Communication Error: " + ex, "TwitchPlugin"); 74 | return false; 75 | } 76 | return true; 77 | } 78 | 79 | public void JoinChannel(string channel) => SendData("JOIN", "#" + channel); 80 | 81 | public void LeaveChannel(string channel) => SendData("PART", "#" + channel); 82 | 83 | public void Quit() => SendData("QUIT"); 84 | 85 | public void SendMessage(string channel, string message) => SendData("PRIVMSG #" + channel, ":" + message); 86 | 87 | private void Listen() 88 | { 89 | string data; 90 | while(_connection.Connected && (data = _reader.ReadLine()) != null) 91 | { 92 | try 93 | { 94 | var dataParts = data.Split(' '); 95 | if(dataParts[0] == "PING") 96 | { 97 | SendData("PONG", dataParts[1]); 98 | continue; 99 | } 100 | if(dataParts[1] == "PRIVMSG") 101 | { 102 | var msg = data.Split(':')[2]; 103 | var user = dataParts[0].Remove(0, 1).Split('!')[0]; 104 | var channel = dataParts[2].Remove(0, 1); 105 | OnChatMsg?.Invoke(new TwitchChatMessage(user, channel, msg)); 106 | } 107 | } 108 | catch(Exception e) 109 | { 110 | Log.Error(e); 111 | } 112 | } 113 | } 114 | 115 | private void SendData(string cmd, string param = "", bool log = true) 116 | { 117 | var data = param == "" ? cmd : cmd + " " + param; 118 | _writer.WriteLine(data); 119 | if(Config.Instance.IrcLogging && log) 120 | Log.Info(data, "TwitchPlugin-IRC"); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /TwitchPlugin/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Reflection; 4 | using System.Runtime.InteropServices; 5 | 6 | #endregion 7 | 8 | // General Information about an assembly is controlled through the following 9 | // set of attributes. Change these attribute values to modify the information 10 | // associated with an assembly. 11 | 12 | [assembly: AssemblyTitle("TwitchPlugin")] 13 | [assembly: AssemblyDescription("")] 14 | [assembly: AssemblyConfiguration("")] 15 | [assembly: AssemblyCompany("")] 16 | [assembly: AssemblyProduct("TwitchPlugin")] 17 | [assembly: AssemblyCopyright("Copyright © 2015")] 18 | [assembly: AssemblyTrademark("")] 19 | [assembly: AssemblyCulture("")] 20 | 21 | // Setting ComVisible to false makes the types in this assembly not visible 22 | // to COM components. If you need to access a type in this assembly from 23 | // COM, set the ComVisible attribute to true on that type. 24 | 25 | [assembly: ComVisible(false)] 26 | 27 | // The following GUID is for the ID of the typelib if this project is exposed to COM 28 | 29 | [assembly: Guid("1e830738-6d29-4bf7-b52c-f71eca7aa609")] 30 | 31 | // Version information for an assembly consists of the following four values: 32 | // 33 | // Major Version 34 | // Minor Version 35 | // Build Number 36 | // Revision 37 | // 38 | // You can specify all the values or you can default the Build and Revision Numbers 39 | // by using the '*' as shown below: 40 | // [assembly: AssemblyVersion("1.0.*")] 41 | 42 | [assembly: AssemblyVersion("0.6.0.0")] 43 | [assembly: AssemblyFileVersion("0.6.0.0")] 44 | -------------------------------------------------------------------------------- /TwitchPlugin/SettingsWindow.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 41 | get oauth key here 42 | 43 | 44 | 46 | 47 | 48 | 49 |