Advertisement
jarppaaja

SalvageFromIcyVeins

Aug 17th, 2019
661
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 14.21 KB | None | 0 0
  1. // SalvageFromIcyVeins.cs "$Revision: 2505 $" "$Date: 2019-08-17 11:03:04 +0300 (la, 17 elo 2019) $"
  2. // https://www.nuget.org/packages/HtmlAgilityPack/
  3. using HtmlAgilityPack;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Diagnostics;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Text;
  10. using System.Xml.XPath;
  11.  
  12. namespace LogDataCollector
  13. {
  14.     public enum HeroClass : int
  15.     {
  16.         DemonHunter = 0,
  17.         Barbarian = 1,
  18.         Wizard = 2,
  19.         WitchDoctor = 3,
  20.         Monk = 4,
  21.         Crusader = 5,
  22.         Necromancer = 6,
  23.         Follower = 7,       // For our convenience only.
  24.         Total = 8,
  25.         None = 9,           // Change Follower -> None for final output.
  26.     }
  27.  
  28.     public class SalvageFromIcyVeins
  29.     {
  30.         private class Build
  31.         {
  32.             public readonly HeroClass HeroClass;
  33.             public readonly List<string> BuildNames;
  34.  
  35.             public Build(HeroClass heroClass, string buildName)
  36.             {
  37.                 HeroClass = heroClass;
  38.                 BuildNames = new List<string>() { buildName };
  39.             }
  40.         }
  41.  
  42.         private const string html = "https://www.icy-veins.com/d3/legendary-item-salvage-guide";
  43.  
  44.         public bool SkipLoN = false;        // You can skip LoN build in the UI as well so include them by default.
  45.         public bool FileTest = false;
  46.         public HashSet<HeroClass> HeroClasses = new HashSet<HeroClass>()
  47.         {
  48.             HeroClass.DemonHunter,
  49.             HeroClass.Barbarian,
  50.             HeroClass.Wizard,
  51.             HeroClass.WitchDoctor,
  52.             HeroClass.Monk,
  53.             HeroClass.Crusader,
  54.             HeroClass.Necromancer,
  55.             HeroClass.Follower,
  56.         };
  57.  
  58.         // Replace with Console.WriteLine for simple work around to write somewhere or {} to disable.
  59.         private void WriteLine(object value) { OUT.WriteLine(value.ToString()); }
  60.         private void WriteLine(string value = null) { OUT.WriteLine(value); }
  61.         private void WriteLine(string format, params object[] arg) { OUT.WriteLine(string.Format(format, arg)); }
  62.  
  63.         public void LoadData(string _dataDir)
  64.         {
  65.             var filename = Path.Combine(_dataDir, "salvage-guide.html");
  66.             var cacheDir = Path.Combine(_dataDir, "htmlCache");
  67.             var salvageItemsDataFile = Path.Combine(_dataDir, "SalvageItemsData.cs");
  68.             if (!Directory.Exists(cacheDir))
  69.             {
  70.                 Directory.CreateDirectory(cacheDir);
  71.             }
  72.             var htmlDoc = (HtmlDocument)null;
  73.             if (FileTest && File.Exists(filename))
  74.             {
  75.                 this.WriteLine("Load HTML from: {0}", html);
  76.                 htmlDoc = new HtmlDocument();
  77.                 htmlDoc.Load(filename);
  78.             }
  79.             else
  80.             {
  81.                 // Must create in this order.
  82.                 var htmlWeb = new HtmlWeb();
  83.                 htmlWeb.CachePath = cacheDir;
  84.                 htmlWeb.UsingCache = true;
  85.  
  86.                 this.WriteLine("Load HTML from: {0}", html);
  87.                 htmlDoc = htmlWeb.Load(html);
  88.                 this.WriteLine("Load HTML took: {0} ms", htmlWeb.RequestDuration);
  89.             }
  90.             var salvageTable = XPathExpression.Compile("//table[@class='salvage_table']");
  91.             var table = htmlDoc.DocumentNode.SelectSingleNode(salvageTable);
  92.             if (table == null)
  93.             {
  94.                 this.WriteLine("TABLE not found in HTML document, expr={0}", salvageTable.Expression);
  95.                 return;
  96.             }
  97.             try
  98.             {
  99.                 Dictionary<string, List<Build>> itemBuilds = new Dictionary<string, List<Build>>();
  100.                 parseTable(table, itemBuilds);
  101.                 printItemsAndBuilds(itemBuilds, salvageItemsDataFile);
  102.             }
  103.             catch (Exception x)
  104.             {
  105.                 this.WriteLine(x);
  106.                 if (Debugger.IsAttached) Debugger.Break();
  107.                 return;
  108.             }
  109.         }
  110.  
  111.         private List<ItemData> loadItemData()
  112.         {
  113.             return new List<ItemData>();
  114.         }
  115.  
  116.         private void parseTable(HtmlNode table, Dictionary<string, List<Build>> itemBuilds)
  117.         {
  118.             var rowCount = 0;
  119.             foreach (var row in table.SelectNodes("//tr"))
  120.             {
  121.                 rowCount += 1;
  122.                 var elements = row.Elements("td");
  123.                 var columns = elements?.ToList();
  124.                 if (columns?.Count != 2)
  125.                 {
  126.                     if (rowCount > 1)
  127.                     {
  128.                         this.WriteLine("row{0,4} INVALID columns={1}", rowCount, columns?.Count);
  129.                     }
  130.                     continue;
  131.                 }
  132.                 // Find item name from link
  133.                 var links = columns[0].SelectNodes("./span/a")?.ToList();
  134.                 if (links?.Count != 1)
  135.                 {
  136.                     this.WriteLine("row{0,4} INVALID links={1}", rowCount, links?.Count);
  137.                     continue;
  138.                 }
  139.                 var itemName = links[0].InnerText;
  140.                 if (itemName.StartsWith("Hellfire "))   // Ignore Hellfire Amulet and Ring because they are so special and must be judged by the user!
  141.                 {
  142.                     this.WriteLine("row{0,4} {1} SKIPPED", rowCount, itemName);
  143.                     continue;
  144.                 }
  145.                 this.WriteLine("row{0,4} {1}", rowCount, itemName);
  146.  
  147.                 // Find build names for unordered list (inside a link).
  148.                 var items = columns[1].SelectNodes("./ul/li")?.ToList();
  149.                 if (items?.Count == 0)
  150.                 {
  151.                     this.WriteLine("row{0,4} INVALID items={1}", rowCount, items?.Count);
  152.                     continue;
  153.                 }
  154.                 if (!itemBuilds.TryGetValue(itemName, out var buildList))
  155.                 {
  156.                     buildList = new List<Build>();
  157.                     itemBuilds.Add(itemName, buildList);
  158.                 }
  159.                 foreach (var item in items)
  160.                 {
  161.                     var buildNameText = item.InnerText.Trim();  // Trim to be sure.
  162.                     while (buildNameText.Contains("  "))        // InnerText can return double spaces which we must fix for parser.
  163.                     {
  164.                         buildNameText = buildNameText.Replace("  ", " ");
  165.                     }
  166.                     if (SkipLoN && buildNameText.StartsWith("LoN "))    // Filter LoN builds
  167.                         continue;
  168.                     if (buildNameText.EndsWith("outdated"))             // outdated!
  169.                         continue;
  170.                     if (buildNameText.EndsWith(" (Cube)"))              // Only for Cube!
  171.                         continue;
  172.                     if (buildNameText.Contains("Dungeon Guide"))        // Set Dungeon Guide skipped!
  173.                         continue;
  174.                     if (buildNameText.Contains("The Thrill"))           // The Thrill Conquest Build!
  175.                         continue;
  176.                     var tuple = parseBuild(buildNameText);
  177.                     if (tuple != null)
  178.                     {
  179.                         var heroClass = tuple.Item1;
  180.                         if (!HeroClasses.Contains(heroClass))           // Filter builds by class.
  181.                             continue;
  182.                         var buildName = tuple.Item2;
  183.                         this.WriteLine("        {0,-11} {1}", heroClass, buildName);
  184.                         var build = buildList.FirstOrDefault(x => x.HeroClass == heroClass);
  185.                         if (build == null)
  186.                         {
  187.                             build = new Build(heroClass, buildName);
  188.                             buildList.Add(build);
  189.                         }
  190.                         else
  191.                         {
  192.                             build.BuildNames.Add(buildName);
  193.                         }
  194.                     }
  195.                 }
  196.             }
  197.             this.WriteLine("{0} rows processed", rowCount);
  198.         }
  199.  
  200.         private void printItemsAndBuilds(Dictionary<string, List<Build>> itemBuilds, string salvageItemsDataFile)
  201.         {
  202.  
  203.             // HeroClass -> build name -> item name
  204.             var counters = new int[(int)HeroClass.Total + 1];
  205.             var totalIndex = (int)HeroClass.Total;
  206.             var builder = new StringBuilder().AppendLine();
  207.             var keys = itemBuilds.Keys.ToList();
  208.             keys.Sort();
  209.  
  210.             builder
  211.                 .Append("    public sealed class SalvageItemsData").AppendLine()
  212.                 .Append("    {").AppendLine()
  213.                 .Append("        public readonly Dictionary<string, Build[]> HeroBuilds = new Dictionary<string, Build[]>()").AppendLine()
  214.                 .Append("        {").AppendLine();
  215.  
  216.             foreach (var itemName in keys)
  217.             {
  218.                 var builds = itemBuilds[itemName];
  219.                 if (builds.Count == 0)
  220.                 {
  221.                     continue;
  222.                 }
  223.                 // Item name.
  224.                 builder
  225.                     .Append("            { ")
  226.                     .AppendFormat("\"{0}\", new Build[] ", itemName)
  227.                     .Append("{")
  228.                     .AppendLine();
  229.                 counters[totalIndex] += 1;
  230.  
  231.                 builds.Sort((a, b) => a.HeroClass.CompareTo(b.HeroClass));
  232.                 foreach (var build in builds)
  233.                 {
  234.                     // Hero class, convert Follower to None.
  235.                     var heroClass = build.HeroClass == HeroClass.Follower ? HeroClass.None : build.HeroClass;
  236.                     builder
  237.                         .AppendFormat("                new Build(HeroClass.{0},", heroClass.ToString())
  238.                         .AppendLine();
  239.  
  240.                     // Build names - sort and loop.
  241.                     build.BuildNames.Sort();
  242.                     var heroIndex = (int)build.HeroClass;
  243.                     var lastIndex = build.BuildNames.Count - 1;
  244.                     for (var i = 0; i < build.BuildNames.Count; ++i)
  245.                     {
  246.                         var buildName = build.BuildNames[i];
  247.                         builder
  248.                             .AppendFormat("                \"{0}\"", buildName);
  249.                         if (i < lastIndex)
  250.                         {
  251.                             builder
  252.                                 .Append(",")
  253.                                 .AppendLine();
  254.                         }
  255.                         else
  256.                         {
  257.                             builder
  258.                                 .Append("),")
  259.                                 .AppendLine();
  260.                         }
  261.                         counters[heroIndex] += 1;
  262.                     }
  263.                 }
  264.                 builder
  265.                     .Append("            }},").AppendLine();
  266.             }
  267.             builder
  268.                 .Append("        };").AppendLine()
  269.                 .Append("    }").AppendLine();
  270.  
  271.             var result = builder.ToString();
  272.             File.WriteAllText(salvageItemsDataFile, result);
  273.  
  274.             this.WriteLine(result);
  275.             this.WriteLine("{0} items processed", keys.Count);
  276.             builder
  277.                 .Clear().AppendLine();
  278.             for (int i = 0; i < counters.Length; ++i)
  279.             {
  280.                 var heroClass = (HeroClass)i;
  281.                 builder
  282.                     .AppendFormat("{0,-11}", heroClass.ToString())
  283.                     .AppendFormat("{0,5}", counters[i])
  284.                     .AppendLine();
  285.             }
  286.             builder
  287.                 .AppendFormat("{0,-11}", "Skipped")
  288.                 .AppendFormat("{0,5}", keys.Count - counters[totalIndex])
  289.                 .AppendLine();
  290.             this.WriteLine(builder.ToString());
  291.         }
  292.  
  293.         private static Tuple<HeroClass, string> parseBuild(string buildNameText)
  294.         {
  295.             string[] tokens = parseAsTokens(buildNameText);
  296.             if (!Enum.TryParse(tokens[1], out HeroClass heroClass))
  297.                 throw new InvalidOperationException(tokens[1]);
  298.             var name = tokens[0];
  299.             if (name == "Support")
  300.             {
  301.                 name += " " + tokens[1];    // Support build is class specific!
  302.             }
  303.             var spec = tokens[2];
  304.             spec = spec.Replace(" + ", "+").Replace(" variation)", ")").Replace(" variations)", ")");
  305.  
  306.             return new Tuple<HeroClass, string>(heroClass, name + " " + spec);
  307.         }
  308.  
  309.         private static string[] parseAsTokens(string buildName)
  310.         {
  311.             var pos2 = buildName.IndexOf(" (");
  312.             if (pos2 == -1)
  313.                 throw new InvalidOperationException(buildName);
  314.             var pos1 = buildName.IndexOf(" Demon Hunter (");
  315.             if (pos1 == -1)
  316.                 pos1 = buildName.IndexOf(" Barbarian (");
  317.             if (pos1 == -1)
  318.                 pos1 = buildName.IndexOf(" Wizard (");
  319.             if (pos1 == -1)
  320.                 pos1 = buildName.IndexOf(" Witch Doctor (");
  321.             if (pos1 == -1)
  322.                 pos1 = buildName.IndexOf(" Monk (");
  323.             if (pos1 == -1)
  324.                 pos1 = buildName.IndexOf(" Crusader (");
  325.             if (pos1 == -1)
  326.                 pos1 = buildName.IndexOf(" Necromancer (");
  327.             if (pos1 == -1 && buildName.Contains("Follower Guide ("))
  328.             {
  329.                 return new string[] {
  330.                     "Follower",
  331.                     HeroClass.Follower.ToString(),
  332.                     buildName.Substring(pos2 + 1),
  333.                 };
  334.             }
  335.             if (pos1 == -1)
  336.                 throw new InvalidOperationException(buildName);
  337.             return new string[] {
  338.                 buildName.Substring(0, pos1),
  339.                 buildName.Substring(pos1 + 1, pos2 - pos1 - 1).Replace(" ", ""),
  340.                 buildName.Substring(pos2 + 1),
  341.             };
  342.         }
  343.  
  344.         private static string parseBaseName(string buildName)
  345.         {
  346.             var pos = buildName.IndexOf(" (");
  347.             if (pos == -1)
  348.                 throw new InvalidOperationException(buildName);
  349.             return buildName.Substring(0, pos);
  350.         }
  351.     }
  352. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement