Example #1
0
        private static void OutputModel(BisModel model, TextWriter writer)
        {
            writer.WriteLine("Gear: ");
            model.ChosenGear.ForEach(
                kv =>
                writer.WriteLine("\t" + string.Join(" or ", kv.EquipSlotCategory.PossibleSlots.OrderBy(s => s.ToString())) +
                                 ": " + kv));

            if (model.ChosenMateria.Any())
            {
                writer.WriteLine("Materia: ");
                model.ChosenMateria.ForEach(
                    kv =>
                    writer.WriteLine("\t" + kv.Item1 + ": " + kv.Item2 + ",\n\t\t - Materia: " + kv.Item3 +
                                     "\n\t\t\t   Amount: " + kv.Item4));
            }

            if (model.ChosenRelicStats.Any())
            {
                writer.WriteLine("Relic distribution: ");
                model.ChosenRelicDistribution.ForEach(
                    kv => writer.WriteLine("\t" + kv.Item1 + " - " + kv.Item2 + ": " + kv.Item3));
                writer.WriteLine("Relic stats: ");
                model.ChosenRelicStats.ForEach(kv => writer.WriteLine("\t" + kv.Item1 + " - " + kv.Item2 + ": " + kv.Item3));
            }

            if (model.ChosenFood != null)
            {
                writer.WriteLine("Food: ");
                writer.WriteLine("\t" + model.ChosenFood);
            }
            writer.WriteLine("Allocated stats: ");
            model.ResultAllocatableStats.ForEach(kv => writer.WriteLine("\t" + kv.Key + ": " + kv.Value));
            writer.WriteLine("Result stats with food:");
            model.ResultTotalStats.ForEach(kv => writer.WriteLine("\t" + kv.Key + ": " + kv.Value));
            writer.WriteLine($"Result stat weight: {model.ResultWeight}");
        }
Example #2
0
        //TODO: make this more pretty
        public static void Main(string[] args)
        {
            var cliApp     = new CommandLineApplication();
            var xivPathOpt = cliApp.Option("-p |--game-path <pathToFFXIV>",
                                           "Path to the FFXIV game install (folder containing boot and game)", CommandOptionType.SingleValue);

            var configOpt = cliApp.Option("-c |--config-path <pathToYaml>",
                                          "Path to configuration YAML file, default to config.yaml", CommandOptionType.SingleValue);

            var excludedOpt = cliApp.Option("-X |--exclude <itemId>",
                                            "Item ids of gear or food to exclude from solving; repeat for non-unique items", CommandOptionType.MultipleValue);

            var requiredOpt = cliApp.Option("-R |--require <itemId>",
                                            "Item ids of items required when solving", CommandOptionType.MultipleValue);

            var minIlvlOpt = cliApp.Option("-m |--min-itemlevel <ilvl>",
                                           "Minimum item level of items to consider. Uses max-20 if not passed.", CommandOptionType.SingleValue);
            var maxIlvlOpt = cliApp.Option("-M |--max-itemlevel <ilvl>",
                                           "Maximum item level of items to consider", CommandOptionType.SingleValue);

            var maxOvermeldTierOpt = cliApp.Option("-T |--max-overmeld-tier <tier>",
                                                   "The max tier of materia allowed for overmelds", CommandOptionType.SingleValue);

            var noMaximizeUnweightedOpt = cliApp.Option("--no-maximize-unweighted",
                                                        "Choose to disable maximizing unweighted stats (usually accuracy). Shouldn't be needed.",
                                                        CommandOptionType.NoValue);

            var noFoodOpt = cliApp.Option("--no-food", "Disable food", CommandOptionType.NoValue);

            var noMateriaOpt = cliApp.Option("--no-materia", "Disable materia", CommandOptionType.NoValue);

            var noRelicOpt = cliApp.Option("--no-relic", "Disable relic", CommandOptionType.NoValue);

            var tiersOpt = cliApp.Option("--use-tiers", "Enable SS tiers. Warning: slow unless using a commercial solver", CommandOptionType.NoValue);

            var outputOpt = cliApp.Option("-o |--output <file>", "Write output to <file>", CommandOptionType.SingleValue);

            var solverOpt = cliApp.Option("-s |--solver <solver>", "Solver to use (default: GLPK)",
                                          CommandOptionType.SingleValue);

            var noSolveOpt = cliApp.Option("--no-solve", "Don't solve the model; only works in conjunction with --debug", CommandOptionType.NoValue);

            var debugOpt = cliApp.Option("-d |--debug", "Print the used models in the current directory as model.lp",
                                         CommandOptionType.NoValue);

            var jobArg = cliApp.Argument("<job>", "Enter the job abbreviation to solve for");

            cliApp.HelpOption("-h |--help");

            cliApp.OnExecute(() =>
            {
                if (jobArg.Value == null)
                {
                    Console.Error.WriteLine("You must provide a job to solve for.");
                    return(1);
                }

                if (!xivPathOpt.HasValue())
                {
                    Console.Error.WriteLine("You must provide a path to FFXIV!");
                    return(1);
                }

                var realm   = new ARealmReversed(xivPathOpt.Value(), Language.English);
                var xivColl = realm.GameData;

                //TODO: can combine those converters
                var deserializer = new DeserializerBuilder()
                                   .WithTypeConverter(new BaseParamConverter(xivColl))
                                   .WithTypeConverter(new ClassJobConverter(xivColl))
                                   .WithTypeConverter(new EquipSlotConverter(xivColl))
                                   .WithTypeConverter(new ItemConverter(xivColl))
                                   .WithTypeConverter(new PiecewiseLinearConverter())
                                   .WithNamingConvention(new CamelCaseNamingConvention())
                                   .Build();

                SolverConfig solverConfig = null;

                using (var s = new FileStream(configOpt.HasValue() ? configOpt.Value() : "config.yaml", FileMode.Open))
                {
                    solverConfig = deserializer.Deserialize <SolverConfig>(new StreamReader(s));
                }

                solverConfig.MaximizeUnweightedValues = !noMaximizeUnweightedOpt.HasValue();
                solverConfig.UseTiers = tiersOpt.HasValue();

                var classJob = xivColl.GetSheet <ClassJob>().Single(x => string.Equals(x.Abbreviation, jobArg.Value, StringComparison.InvariantCultureIgnoreCase));

                var items = xivColl.GetSheet <Item>().ToList();

                if (excludedOpt.HasValue())
                {
                    var excludedIds = new List <int>();
                    foreach (var excluded in excludedOpt.Values)
                    {
                        try
                        {
                            var id   = int.Parse(excluded);
                            var item = xivColl.Items[id];
                            excludedIds.Add(id);
                        }
                        catch (KeyNotFoundException)
                        {
                            Console.Error.WriteLine($"Unknown id {excluded}, ignoring.");
                        }
                        catch (FormatException)
                        {
                            Console.Error.WriteLine($"Not an integer: {excluded}");
                        }
                        catch (OverflowException)
                        {
                            Console.Error.WriteLine($"Too large: {excluded}");
                        }
                    }
                    items = items.Where(k => !excludedIds.Contains(k.Key)).ToList();
                }

                //TODO: duplicated code
                if (requiredOpt.HasValue())
                {
                    solverConfig.RequiredItems = new List <int>();
                    requiredOpt.Values.Select(int.Parse).ForEach(solverConfig.RequiredItems.Add);
                }

                var equip = items.OfType <Equipment>().Where(e => e.ClassJobCategory.ClassJobs.Contains(classJob));

                var maxIlvl = equip.Max(x => x.ItemLevel.Key);
                if (maxIlvlOpt.HasValue())
                {
                    maxIlvl = int.Parse(maxIlvlOpt.Value());
                }

                var minIlvl = maxIlvl - 20;
                if (minIlvlOpt.HasValue())
                {
                    minIlvl = int.Parse(minIlvlOpt.Value());
                }

                equip = equip.Where(e => e.ItemLevel.Key >= minIlvl && e.ItemLevel.Key <= maxIlvl || solverConfig.RequiredItems != null && solverConfig.RequiredItems.Contains(e.Key)).ToList();

                var food = noFoodOpt.HasValue() ? new List <FoodItem>() : items.Where(FoodItem.IsFoodItem).Select(t => new FoodItem(t));

                var maxTier = items.OfType <MateriaItem>().Max(i => i.Tier);
                var materia = noMateriaOpt.HasValue() ? new Dictionary <MateriaItem, bool>() : items.OfType <MateriaItem>()
                              .Where(i => i.Tier == maxTier || (maxOvermeldTierOpt.HasValue() && i.Tier == int.Parse(maxOvermeldTierOpt.Value()) - 1))
                              .ToDictionary(i => i, i => !maxOvermeldTierOpt.HasValue() || i.Tier < int.Parse(maxOvermeldTierOpt.Value()));

                if (noRelicOpt.HasValue())
                {
                    solverConfig.RelicConfigs = new Dictionary <int, RelicConfig>();
                }

                //TODO: improve solver handling
                SolverBase solver = CreateGLPKSolver();
                if (solverOpt.HasValue())
                {
                    switch (solverOpt.Value())
                    {
                    case "Gurobi":
                        solver = new GurobiSolver();
                        solverConfig.SolverSupportsSOS = true;
                        break;
                    }
                }

                var debug    = debugOpt.HasValue();
                var settings = new OptimizationConfigSection();
                //settings.ModelElement.EnableFullNames = debug;

                using (var scope = new ModelScope(settings))
                {
                    var model = new BisModel(solverConfig, classJob,
                                             equip, food, materia);

                    if (debug)
                    {
                        using (var f = new FileStream("model.lp", FileMode.Create))
                        {
                            model.Model.Write(f, FileType.LP);
                        }
                        using (var f = new FileStream("model.mps", FileMode.Create))
                        {
                            model.Model.Write(f, FileType.MPS);
                        }

                        if (noSolveOpt.HasValue())
                        {
                            Console.WriteLine("Printed model, exiting...");
                            return(0);
                        }
                    }

                    var solution = solver.Solve(model.Model);

                    model.ApplySolution(solution);

                    if (outputOpt.HasValue())
                    {
                        using (var fs = new FileStream(outputOpt.Value(), FileMode.Create))
                        {
                            var sw = new StreamWriter(fs);
                            OutputModel(model, sw);
                            sw.Close();
                        }
                    }
                    else
                    {
                        OutputModel(model, Console.Out);
                    }
                    Console.WriteLine(solverConfig.UseTiers ? "SS tiers have been taken into account" : "SS tiers have been ignored; pass --use-tiers to enable (slow)");
                }

                return(0);
            });

            cliApp.Execute(args);
        }
        //TODO: make this more pretty
        public static void Main(string[] args)
        {
            var cliApp     = new CommandLineApplication();
            var xivPathOpt = cliApp.Option("-p |--game-path <pathToFFXIV>",
                                           "Path to the FFXIV game install (folder containing boot and game)", CommandOptionType.SingleValue);

            var configOpt = cliApp.Option("-c |--config-path <pathToYaml>",
                                          "Path to configuration YAML file, default to config.yaml", CommandOptionType.SingleValue);

            var excludedOpt = cliApp.Option("-X |--exclude <itemId>",
                                            "Item ids of items to exclude from solving", CommandOptionType.MultipleValue);

            var minIlvlOpt = cliApp.Option("-m |--min-itemlevel <ilvl>",
                                           "Minimum item level of items to consider. Uses max-20 if not passed.", CommandOptionType.SingleValue);
            var maxIlvlOpt = cliApp.Option("-M |--max-itemlevel <ilvl>",
                                           "Maximum item level of items to consider", CommandOptionType.SingleValue);

            var maxOvermeldTierOpt = cliApp.Option("-T |--max-overmeld-tier <tier>",
                                                   "The max tier of materia allowed for overmelds", CommandOptionType.SingleValue);

            var noMaximizeUnweightedOpt = cliApp.Option("--no-maximize-unweighted",
                                                        "Choose to disable maximizing unweighted stats (usually accuracy). Shouldn't be needed.",
                                                        CommandOptionType.NoValue);

            var solverOpt = cliApp.Option("-s |--solver <solver>", "Solver to use (default: GLPK)",
                                          CommandOptionType.SingleValue);

            var debugOpt = cliApp.Option("-d |--debug", "Print the used models in the current directory as model.lp",
                                         CommandOptionType.NoValue);

            var jobArg = cliApp.Argument("<job>", "Enter the job abbreviation to solve for");

            cliApp.HelpOption("-h |--help");

            cliApp.OnExecute(() =>
            {
                if (jobArg.Value == null)
                {
                    Console.Error.WriteLine("You must provide a job to solve for.");
                    return(1);
                }

                if (!xivPathOpt.HasValue())
                {
                    Console.Error.WriteLine("You must provide a path to FFXIV!");
                    return(1);
                }

                var realm   = new ARealmReversed(xivPathOpt.Value(), Language.English);
                var xivColl = realm.GameData;

                var deserializer = new DeserializerBuilder()
                                   .WithTypeConverter(new BaseParamConverter(xivColl))
                                   .WithTypeConverter(new ClassJobConverter(xivColl))
                                   .WithNamingConvention(new CamelCaseNamingConvention())
                                   .Build();

                AppConfig config = null;

                using (var s = new FileStream(configOpt.HasValue() ? configOpt.Value() : "config.yaml", FileMode.Open))
                {
                    config = deserializer.Deserialize <AppConfig>(new StreamReader(s));
                }

                var classJob = xivColl.GetSheet <ClassJob>().Single(x => x.Abbreviation == jobArg.Value);

                var jobConfig = config.JobConfigs[classJob];

                var items = xivColl.GetSheet <Item>().ToList();

                if (excludedOpt.HasValue())
                {
                    var excludedIds = new List <int>();
                    foreach (var excluded in excludedOpt.Values)
                    {
                        var id   = int.Parse(excluded);
                        var item = xivColl.Items[id];
                        if (item != null)
                        {
                            Console.WriteLine($"Excluding {item}.");
                            excludedIds.Add(id);
                        }
                        else
                        {
                            Console.Error.WriteLine($"Unknown id {id}, ignoring.");
                        }
                    }
                    items = items.Where(k => !excludedIds.Contains(k.Key)).ToList();
                }

                var equip = items.OfType <Equipment>().Where(e => e.ClassJobCategory.ClassJobs.Contains(classJob));

                var maxIlvl = equip.Max(x => x.ItemLevel.Key);
                if (maxIlvlOpt.HasValue())
                {
                    maxIlvl = int.Parse(maxIlvlOpt.Value());
                }

                var minIlvl = maxIlvl - 20;
                if (minIlvlOpt.HasValue())
                {
                    minIlvl = int.Parse(minIlvlOpt.Value());
                }

                equip = equip.Where(e => e.ItemLevel.Key >= minIlvl && e.ItemLevel.Key <= maxIlvl).ToList();

                var food = items.Where(FoodItem.IsFoodItem).Select(t => new FoodItem(t));

                var materia = items.OfType <MateriaItem>()
                              .ToDictionary(i => i,
                                            i => !maxOvermeldTierOpt.HasValue() || i.Tier < int.Parse(maxOvermeldTierOpt.Value()));

                var relicCaps =
                    equip.Where(e => config.RelicCaps.ContainsKey(e.ItemLevel.Key))
                    .ToDictionary(e => e, e => config.RelicCaps[e.ItemLevel.Key]);

                //TODO: improve solver handling
                SolverBase solver = new GLPKSolver();
                if (solverOpt.HasValue())
                {
                    switch (solverOpt.Value())
                    {
                    case "Gurobi":
                        solver = new GurobiSolver();
                        break;

                    case "Z3":
                        solver = new Z3Solver();
                        break;
                    }
                }


                using (var scope = new ModelScope())
                {
                    var model = new BisModel(jobConfig.Weights, jobConfig.StatRequirements, config.BaseStats,
                                             equip, food, materia, relicCaps, maximizeUnweightedValues: !noMaximizeUnweightedOpt.HasValue());

                    if (debugOpt.HasValue())
                    {
                        using (var f = new FileStream("model.lp", FileMode.Create))
                        {
                            var obj        = model.Model.Objectives.First();
                            obj.Expression = obj.Expression.Normalize();
                            model.Model.Constraints.ForEach(c => c.Expression = c.Expression.Normalize());
                            model.Model.Write(f, FileType.LP);
                        }
                    }

                    var solution = solver.Solve(model.Model);
                    model.ApplySolution(solution);
                    Console.WriteLine("Gear: ");
                    model.ChosenGear.ForEach(Console.WriteLine);
                    Console.WriteLine("Materia: ");
                    model.ChosenMateria.ForEach(Console.WriteLine);
                    if (model.ChosenRelicStats.Any())
                    {
                        Console.WriteLine("Relic stats: ");
                        model.ChosenRelicStats.ForEach(Console.WriteLine);
                    }
                    Console.WriteLine("Food: ");
                    Console.WriteLine(model.ChosenFood);
                    Console.WriteLine("Allocated stats: ");
                    model.ResultAllocatableStats.ForEach(kv => Console.WriteLine(kv));
                    Console.WriteLine("Result stats with food:");
                    model.ResultTotalStats.ForEach(kv => Console.WriteLine(kv));
                    Console.WriteLine($"Result stat weight: {model.ResultWeight}");
                }

                return(0);
            });

            cliApp.Execute(args);
        }