Example #1
0
        public Result FindItems(RandomizerOptions opt, AnnotationData ann, Graph g, Events events, string gameDir, FromGame game)
        {
            Dictionary <string, string>         itemsById = ann.KeyItems.ToDictionary(item => item.ID, item => item.Name);
            Dictionary <string, List <string> > itemAreas = g.ItemAreas;

            if (ann.LotLocations != null)
            {
                GameEditor editor = new GameEditor(game);
                editor.Spec.GameDir = gameDir;
                Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts();
                Dictionary <string, PARAM>        Params  = editor.LoadParams(layouts);

                Dictionary <int, PARAM.Row> lots = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r);
                foreach (KeyValuePair <int, string> entry in ann.LotLocations)
                {
                    int lot = entry.Key;
                    if (!g.Areas.ContainsKey(entry.Value))
                    {
                        throw new Exception($"Internal error in lot config for {entry.Key}: {entry.Value} does not exist");
                    }
                    while (true)
                    {
                        // It's also fine to not have a lot defined as long as all key items are found
                        if (!lots.TryGetValue(lot, out PARAM.Row row))
                        {
                            break;
                        }
                        for (int i = 1; i <= 8; i++)
                        {
                            int item = (int)row[$"lotItemId0{i}"].Value;
                            if (item == 0)
                            {
                                continue;
                            }
                            int category = (int)row[$"lotItemCategory0{i}"].Value;
                            category = Universe.LotTypes.TryGetValue((uint)category, out int value) ? value : -1;
                            if (category == -1)
                            {
                                continue;
                            }
                            string id = $"{category}:{item}";
                            if (opt["debuglots"])
                            {
                                Console.WriteLine($"{entry.Key} in {entry.Value} has {id}");
                            }
                            if (itemsById.TryGetValue(id, out string name))
                            {
                                if (itemAreas[name].Count > 0 && itemAreas[name][0] != entry.Value)
                                {
                                    throw new Exception($"Item {name} found in both {itemAreas[name][0]} and {entry.Value}");
                                }
                                itemAreas[name] = new List <string> {
                                    entry.Value
                                };
                            }
                        }
                        lot++;
                    }
                }
            }
            if (ann.Locations != null)
            {
                // It's a bit hacky, but should work from anywhere, probably
                GameEditor editor = new GameEditor(game);
                editor.Spec.GameDir   = $@"fogdist";
                editor.Spec.LayoutDir = $@"fogdist\Layouts";
                editor.Spec.NameDir   = $@"fogdist\Names";

                Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts();

                int dragonFlag = -1;
                Dictionary <string, PARAM> Params = editor.LoadParams(@"fogdist\Base\Data0.bdt", layouts, true);
                if (gameDir != null)
                {
                    string paramPath = $@"{gameDir}\Data0.bdt";
                    if (File.Exists(paramPath))
                    {
                        Params = editor.LoadParams(paramPath, layouts, true);
                    }

                    string commonEmevdPath = $@"{gameDir}\event\common.emevd.dcx";
                    if (File.Exists(commonEmevdPath))
                    {
                        EMEVD       commonEmevd = EMEVD.Read(commonEmevdPath);
                        EMEVD.Event flagEvent   = commonEmevd.Events.Find(e => e.ID == 13000904);
                        if (flagEvent != null)
                        {
                            Events.Instr check = events.Parse(flagEvent.Instructions[1]);
                            dragonFlag = (int)check[3];
                            if (opt["debuglots"])
                            {
                                Console.WriteLine($"Dragon flag: {dragonFlag}");
                            }
                        }
                    }
                }

                Dictionary <int, PARAM.Row> lots  = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r);
                Dictionary <int, PARAM.Row> shops = Params["ShopLineupParam"].Rows.ToDictionary(r => (int)r.ID, r => r);

                void setArea(string itemName, List <string> areas)
                {
                    if (opt["debuglots"])
                    {
                        Console.WriteLine($"-- name: {itemName}");
                    }
                    if (itemAreas[itemName].Count > 0 && !itemAreas[itemName].SequenceEqual(areas))
                    {
                        throw new Exception($"Item {itemName} found in both {string.Join(",", itemAreas[itemName])} and {string.Join(",", areas)}");
                    }
                    itemAreas[itemName] = areas;
                }

                foreach (KeyItemLoc loc in ann.Locations.Items)
                {
                    List <string> areas = loc.Area.Split(' ').ToList();
                    if (!areas.All(a => g.Areas.ContainsKey(a) || itemAreas.ContainsKey(a)))
                    {
                        // Currently happens with multi-area intersection lots/shops
                        throw new Exception($"Warning: Areas not found for {loc.Area} - {loc.DebugText[0]}");
                    }
                    List <int> lotIds = loc.Lots == null ? new List <int>() : loc.Lots.Split(' ').Select(i => int.Parse(i)).ToList();
                    foreach (int baseLot in lotIds)
                    {
                        int lot = baseLot;
                        while (true)
                        {
                            // It's also fine to not have a lot defined as long as all key items are found
                            if (!lots.TryGetValue(lot, out PARAM.Row row))
                            {
                                break;
                            }
                            for (int i = 1; i <= 8; i++)
                            {
                                int item = (int)row[$"ItemLotId{i}"].Value;
                                if (item == 0)
                                {
                                    continue;
                                }
                                uint category = (uint)row[$"LotItemCategory0{i}"].Value;
                                if (!Universe.LotTypes.TryGetValue(category, out int catVal))
                                {
                                    continue;
                                }
                                string id = $"{catVal}:{item}";
                                if (opt["debuglots"])
                                {
                                    Console.WriteLine($"lot {lot} in {loc.Area} has {id}");
                                }
                                if (itemsById.TryGetValue(id, out string name))
                                {
                                    setArea(name, areas);
                                }
                            }
                            if (dragonFlag > 0 && (int)row["getItemFlagId"].Value == dragonFlag)
                            {
                                setArea("pathofthedragon", areas);
                            }
                            lot++;
                        }
                    }
                    List <int> shopIds = loc.Shops == null ? new List <int>() : loc.Shops.Split(' ').Select(i => int.Parse(i)).ToList();
                    foreach (int shopId in shopIds)
                    {
                        // Not as fine for a shop to be missing, but also whatever
                        if (!shops.TryGetValue(shopId, out PARAM.Row row))
                        {
                            continue;
                        }
                        int    item   = (int)row["EquipId"].Value;
                        int    catVal = (byte)row["equipType"].Value;
                        string id     = $"{catVal}:{item}";
                        if (opt["debuglots"])
                        {
                            Console.WriteLine($"shop {shopId} in {loc.Area} has {id}");
                        }
                        if (itemsById.TryGetValue(id, out string name))
                        {
                            setArea(name, areas);
                        }
                        if (dragonFlag > 0 && (int)row["EventFlag"].Value == dragonFlag)
                        {
                            setArea("pathofthedragon", areas);
                        }
                    }
                }
            }
            // lots:.*[1-9]\r
            // Iterative approach for items which depend simply on other items
            // Recursion would look a lot nicer but lazy
            bool itemExpanded;

            do
            {
                itemExpanded = false;
                foreach (KeyValuePair <string, List <string> > entry in itemAreas)
                {
                    foreach (string dep in entry.Value.ToList())
                    {
                        if (itemAreas.TryGetValue(dep, out List <string> deps))
                        {
                            entry.Value.Remove(dep);
                            entry.Value.AddRange(deps);
                            itemExpanded = true;
                        }
                    }
                }
            }while (itemExpanded);

            if (opt["explain"] || opt["debuglots"])
            {
                foreach (Item item in ann.KeyItems)
                {
                    Console.WriteLine($"{item.Name} {item.ID}: default {item.Area}, found [{string.Join(", ", itemAreas[item.Name])}]");
                }
            }
            // Collect items in graph
            SortedSet <string> itemRecord = new SortedSet <string>();
            bool randomized = false;

            foreach (Item item in ann.KeyItems)
            {
                if (itemAreas[item.Name].Count == 0)
                {
                    if (item.HasTag("randomonly"))
                    {
                        itemAreas[item.Name] = new List <string> {
                            item.Area
                        };
                    }
                    else if (item.HasTag("hard") && !opt["hard"])
                    {
                        continue;
                    }
                    else
                    {
                        throw new Exception($"Couldn't find {item.Name} in item lots");
                    }
                }
                List <string> areas = itemAreas[item.Name];
                foreach (string area in areas)
                {
                    g.Nodes[area].Items.Add(item.Name);
                }
                if (!item.HasTag("randomonly"))
                {
                    if (areas.Count > 1 || areas[0] != item.Area)
                    {
                        randomized = true;
                    }
                    itemRecord.Add($"{item.Name}={string.Join(",", areas)}");
                }
            }
            return(new Result
            {
                Randomized = randomized,
                ItemHash = (RandomizerOptions.JavaStringHash($"{string.Join(";", itemRecord)}") % 99999).ToString().PadLeft(5, '0')
            });
        }
Example #2
0
        public void Connect(RandomizerOptions opt, Graph g, AnnotationData ann)
        {
            Dictionary <string, Node> graph = g.Nodes;
            List <Edge> allFroms            = graph.Values.SelectMany(node => node.From.Where(e => e.From == null)).ToList();
            List <Edge> allTos        = graph.Values.SelectMany(node => node.To.Where(e => e.To == null)).ToList();
            Random      shuffleRandom = new Random(opt.Seed);

            Shuffle(shuffleRandom, allFroms);
            Shuffle(shuffleRandom, allTos);

            // For now, try to connect one-way to one-way and have distinct silos.
            foreach (EdgeSilo siloType in Enum.GetValues(typeof(EdgeSilo)))
            {
                List <Edge> froms = allFroms.Where(e => (e.Pair == null) == (siloType == EdgeSilo.UNPAIRED)).ToList();
                List <Edge> tos   = allTos.Where(e => (e.Pair == null) == (siloType == EdgeSilo.UNPAIRED)).ToList();
                if (opt["explain"])
                {
                    Console.WriteLine($"Connecting silo {siloType}: {froms.Count} with no from, and {tos.Count} with no to");
                }

                while (true)
                {
                    if (opt["vanilla"])
                    {
                        break;
                    }
                    Edge from = null;
                    for (int i = 0; i < froms.Count; i++)
                    {
                        from = froms[i];
                        if (from.From != null)
                        {
                            throw new Exception($"Connected edge still left: {from}");
                        }
                        froms.RemoveAt(i);
                        tos.Remove(from.Pair);
                        break;
                    }
                    if (from == null)
                    {
                        break;
                    }
                    Edge to = null;
                    if (tos.Count == 0)
                    {
                        if (from.Pair != null)
                        {
                            // Have to connect edge to itself
                            to = from.Pair;
                        }
                        else
                        {
                            throw new Exception($"Ran out of eligible edges");
                        }
                    }
                    for (int i = 0; i < tos.Count; i++)
                    {
                        Edge cand = tos[i];
                        if (cand.To != null)
                        {
                            throw new Exception($"Connected edge still left: {cand}");
                        }
                        // Avoid connecting to self
                        if (from.Pair == cand)
                        {
                            continue;
                        }
                        if ((from.Pair == null) != (cand.Pair == null))
                        {
                            continue;
                        }
                        to = cand;
                        tos.RemoveAt(i);
                        froms.Remove(to.Pair);
                        break;
                    }
                    if (to == null)
                    {
                        break;
                    }
                    if (from.IsFixed || to.IsFixed)
                    {
                        throw new Exception($"Internal error: found fixed edges in randomization {from} ({from.IsFixed}) and {to} ({to.IsFixed})");
                    }
                    g.Connect(to, from);
                }
                if (froms.Count > 0 || tos.Count > 0)
                {
                    throw new Exception($"Internal error: unconnected edges after randomization:\nFrom edges: {string.Join(", ", froms)}\nTo edges: {string.Join(", ", tos)}");
                }
            }

            if (opt["start"])
            {
                g.Start = ann.CustomStarts[new Random(opt.Seed - 1).Next(ann.CustomStarts.Count)];
            }
            else if (g.Areas.ContainsKey("asylum"))
            {
                g.Start = new CustomStart
                {
                    Name    = "Asylum",
                    Area    = "asylum",
                    Respawn = "asylum 1812961",
                };
            }
            else if (g.Areas.ContainsKey("firelink_cemetery"))
            {
                g.Start = new CustomStart
                {
                    Name    = "Cemetery of Ash",
                    Area    = "firelink_cemetery",
                    Respawn = "firelink 1812961",
                };
            }
            string start = g.Start.Area;

            // Massive pile of edge-swapping heuristics incoming
            int           tries      = 0;
            GraphChecker  checker    = new GraphChecker();
            CheckRecord   check      = null;
            bool          pairedOnly = !opt["unconnected"];
            List <string> triedSwaps = new List <string>();

            while (tries++ < 100)
            {
                if (opt["explain"])
                {
                    Console.WriteLine($"------------------------ Try {tries}");
                }
                check = checker.Check(opt, g, start);
                if (check.Unvisited.Count == 0 && g.Areas.ContainsKey("firelink_cemetery"))
                {
                    // Try to minimize distance to Firelink Shrine in DS3
                    // This is done by swapping equivalent pairs of areas, matching random cand edge count with random subst edge count, though preferably no additional fixed exits in cand.
                    // The first priority is to have Firelink available. If it can be made accessible before the 10th nontrivial area (although not the first, if there's an option), this is done.
                    // If not, or if in a high enough try, and tree skip is available, firelink_bellfront is made available instead.
                    // Finally if not, it will just be placed as early as possible.
                    // The second priority is to find coiled sword.
                    // If in a map with random entrances, place it as early as possible without replacing Firelink.
                    // If in Bell Tower, repeat for firelink_bellfront and Bell Tower key location
                    bool didSwap = false;

                    List <string> areaOrder = check.Records.Values.OrderBy(r => r.Dist).Select(r => r.Area).ToList();
                    if (opt["explain"])
                    {
                        Console.WriteLine($"Trying to place Firelink now. Overall order: [{string.Join(",", areaOrder.Select((a, i) => $"{a}:{i}"))}]");
                    }
                    Dictionary <string, int> areaIndex = areaOrder.Select((a, i) => (a, i)).ToDictionary(a => a.Item1, a => a.Item2);

                    int    nontrivialCount = areaOrder.Count(a => !g.Areas[a].HasTag("trivial"));
                    string reasonable      = areaOrder.Where(a => !g.Areas[a].HasTag("trivial")).Skip(nontrivialCount * 15 / 100).FirstOrDefault();
                    int    reasonableIndex = reasonable == null ? areaOrder.Count : areaOrder.IndexOf(reasonable);
                    if (opt["explain"])
                    {
                        Console.WriteLine($"Last reasonable area for Firelink requisites: {reasonable}. Total count {areaOrder.Where(a => !g.Areas[a].HasTag("trivial")).Count()}");
                    }

                    Dictionary <string, int>         randomIn   = new Dictionary <string, int>();
                    Dictionary <int, List <string> > byRandomIn = new Dictionary <int, List <string> >();
                    foreach (string area in areaOrder)
                    {
                        Node node  = graph[area];
                        int  count = node.From.Count(e => !e.IsFixed && (opt["unconnected"] || e.Pair != null));
                        // Console.WriteLine($"end time: {area}. {node.From.Count(e => !e.IsFixed && e.Pair != null)}/{node.From.Count} in, {node.To.Count(e => !e.IsFixed && e.Pair != null)}/{node.To.Count} out, trivial {g.Areas[area].HasTag("trivial")}");
                        randomIn[area] = count;
                        AddMulti(byRandomIn, count, area);
                    }
                    bool tryPlace(string subst, bool reasonableOnly, List <string> root = null)
                    {
                        if (areaIndex[subst] <= reasonableIndex)
                        {
                            return(true);
                        }

                        // Note: These should be in area order
                        List <string> cands = byRandomIn[randomIn[subst]].ToList();

                        cands.Remove(subst);
                        if (root != null)
                        {
                            cands.RemoveAll(c => root.Contains(c) && areaIndex[c] < areaIndex[subst]);
                        }
                        if (opt["explain"])
                        {
                            Console.WriteLine($"Candidates for {subst} ({areaIndex[subst]}): {string.Join(",", cands.Select(c => $"{c}:{areaIndex[c]}"))}");
                        }

                        // See if this is necessary
                        // if (excludeSwapTry.ContainsKey(subst)) cands.RemoveAll(c => excludeSwapTry[subst].Contains(c));
                        cands.RemoveAll(c => triedSwaps.Contains(string.Join(",", new SortedSet <string> {
                            subst, c
                        })));
                        if (opt["explain"])
                        {
                            Console.WriteLine($"Candidates for {subst} without tried: {string.Join(",", cands)}");
                        }

                        cands.RemoveAll(area => check.Records[area].InEdge.All(e => e.Key.IsFixed));
                        if (opt["explain"])
                        {
                            Console.WriteLine($"Candidates for {subst} with out edge: {string.Join(",", cands)}");
                        }
                        if (cands.Count == 0)
                        {
                            return(false);
                        }

                        List <string> reasonableCands = cands.Where(c => areaIndex[c] <= reasonableIndex).ToList();

                        if (reasonableCands.Count == 0 && reasonableOnly)
                        {
                            return(false);
                        }

                        string cand = reasonableCands.Count > 1 && areaIndex[cands[0]] <= 1 ? cands[1] : cands[0];

                        if (opt["explain"])
                        {
                            Console.WriteLine($"Final choice: {cand}");
                        }

                        g.SwapConnectedAreas(subst, cand);
                        triedSwaps.Add(string.Join(",", new SortedSet <string> {
                            subst, cand
                        }));
                        didSwap = true;
                        return(true);
                    }

                    // Find all in-going areas for items
                    // Given an area, return all fixed ways to get there, and items/areas required to traverse those paths. Does not need to be recursive.
                    Dictionary <string, List <string> > getFixedIn(string area)
                    {
                        Dictionary <string, List <string> > fixedIn = new Dictionary <string, List <string> >();

                        foreach (Edge fixedEntrance in graph[area].From.Where(e => e.IsFixed))
                        {
                            List <string> reqs = fixedEntrance.LinkedExpr == null ? new List <string>() : fixedEntrance.LinkedExpr.FreeVars().ToList();
                            fixedIn[fixedEntrance.From] = reqs;
                        }
                        return(fixedIn);
                    }

                    if (opt["latewarp"] || opt["instawarp"])
                    {
                        // Guarantee Firelink Shrine placement but not Coiled Sword placement
                        // (in instawarp case, this should be a no-op, and can ignore Coiled Sword logic either way)
                        tryPlace("firelink", true);
                    }
                    else
                    {
                        // Try to place both Firelink and Coiled Sword location, including following item chains
                        bool placedFirelink = tryPlace("firelink", true);
                        // List<string> requiredAreas = new List<string>();
                        List <string> accessibleAreas = new List <string> {
                            "firelink_cemetery"
                        };
                        if (placedFirelink)
                        {
                            accessibleAreas.Add("firelink");
                        }
                        List <string> earlyItems = new List <string> {
                            "coiledsword"
                        };
                        List <string> addedItems     = new List <string>();
                        List <string> earlyItemAreas = new List <string>();
                        bool          foundRoots;
                        do
                        {
                            foreach (string item in earlyItems)
                            {
                                if (!addedItems.Contains(item))
                                {
                                    addedItems.Add(item);
                                    earlyItemAreas.AddRange(g.ItemAreas[item].Except(earlyItemAreas));
                                }
                            }
                            foundRoots = false;
                            foreach (string area in earlyItemAreas.ToList())
                            {
                                Node node = graph[area];
                                // If random entrances exist, we can try to get in through swapping, so no need to chase down roots.
                                if (randomIn[area] > 0)
                                {
                                    continue;
                                }
                                // If no fixed way to get in, that's probably bad, but nothing to do
                                Dictionary <string, List <string> > fixedIn = getFixedIn(area);
                                if (fixedIn.Count == 0)
                                {
                                    continue;
                                }
                                string easyIn = fixedIn.Keys.OrderBy(a => fixedIn[a].Count).First();
                                // Is this always fine?
                                if (!earlyItemAreas.Contains(easyIn) && !accessibleAreas.Contains(easyIn))
                                {
                                    earlyItemAreas.Add(easyIn);
                                    foundRoots = true;
                                }
                                foreach (string moreDep in fixedIn[easyIn])
                                {
                                    if (g.ItemAreas.ContainsKey(moreDep) && !earlyItems.Contains(moreDep))
                                    {
                                        earlyItems.Add(moreDep);
                                        foundRoots = true;
                                    }
                                    else if (graph.ContainsKey(moreDep) && !earlyItemAreas.Contains(moreDep))
                                    {
                                        earlyItemAreas.Add(moreDep);
                                        foundRoots = true;
                                    }
                                }
                            }
                            if (opt["explain"])
                            {
                                Console.WriteLine($"At end of iteration, have items {string.Join(",", earlyItems)} and areas {string.Join(",", earlyItemAreas)}, with adjustable {string.Join(",", earlyItemAreas.Where(a => !accessibleAreas.Contains(a) && randomIn[a] > 0))}");
                            }
                        }while (foundRoots);
                        List <string> placeAreas = earlyItemAreas.Where(a => !accessibleAreas.Contains(a) && randomIn[a] > 0).ToList();
                        if (!placedFirelink)
                        {
                            placeAreas.Insert(0, "firelink");
                        }
                        foreach (string area in placeAreas)
                        {
                            tryPlace(area, false, accessibleAreas);
                            accessibleAreas.Add(area);
                        }
                    }

                    if (didSwap)
                    {
                        continue;
                    }
                    break;
                }

                Edge          toFind    = null;
                List <string> unvisited = check.Unvisited.ToList();
                Shuffle(new Random(opt.Seed + tries), unvisited);
                bool hasCond = true;
                foreach (string area in unvisited)
                {
                    foreach (Edge edge in graph[area].From)
                    {
                        if (!edge.IsFixed && (edge.Pair != null) == pairedOnly)
                        {
                            if (edge.LinkedExpr == null)
                            {
                                toFind  = edge;
                                hasCond = false;
                                break;
                            }
                            else if (toFind == null)
                            {
                                toFind = edge;
                            }
                        }
                    }
                    if (toFind != null && !hasCond)
                    {
                        break;
                    }
                }
                if (toFind == null)
                {
                    if (pairedOnly && opt["warp"])
                    {
                        // Redo but with warp edges instead. Generally only happens with warp-only config.
                        pairedOnly = false;
                        continue;
                    }
                    throw new Exception($"Could not find edge into unreachable areas [{string.Join(", ", check.Unvisited)}] starting from {start}");
                }

                (Edge, float)victim = (null, 0);
                Edge lastEdge  = null;
                int  lastCount = 0;
                foreach (NodeRecord rec in check.Records.Values.OrderBy(r => r.Dist))
                {
                    if (opt["explain"])
                    {
                        Console.WriteLine($"{rec.Area}: {rec.Dist}");
                    }
                    foreach (KeyValuePair <Edge, float> entry in rec.InEdge.OrderBy(e => e.Value))
                    {
                        Edge e = entry.Key;
                        if (opt["explain"])
                        {
                            Console.WriteLine($"  From {e.From}{(e.IsFixed ? " (world)" : "")}: {entry.Value}");
                        }
                    }
                    KeyValuePair <Edge, float> maxEdge = rec.InEdge.OrderBy(e => e.Value).Where(e => !e.Key.IsFixed && (e.Key.Pair != null) == pairedOnly).LastOrDefault();
                    if (maxEdge.Key != null)
                    {
                        int inCount = graph[rec.Area].From.Count;
                        if (inCount > lastCount)
                        {
                            lastEdge  = maxEdge.Key;
                            lastCount = inCount;
                        }
                        KeyValuePair <Edge, float> minEdge = rec.InEdge.OrderBy(e => e.Value).First();
                        if (minEdge.Key != maxEdge.Key)
                        {
                            if (opt["explain"])
                            {
                                Console.WriteLine($"  Min {minEdge.Value}, Max editable {maxEdge.Value}");
                            }
                            // Maybe max victim isn't always best for overall difficulty - or it depends on which edge to swap with is chosen.
                            if (maxEdge.Value >= victim.Item2)
                            {
                                victim = (maxEdge.Key, maxEdge.Value);
                            }
                        }
                    }
                }
                Edge victimEdge = victim.Item1;
                if (victimEdge == null)
                {
                    // We can't preserve original graph structure really, so just pick arbitrary one to change
                    if (lastEdge != null)
                    {
                        if (opt["explain"])
                        {
                            Console.WriteLine("!!!!!!!!!!! Picking non-redundant edge, but last reachable");
                        }
                        victimEdge = lastEdge;
                    }
                    else
                    {
                        // Or, completely pick one indiscriminately even if it goes somewhere important
                        victimEdge = check.Records.Keys.SelectMany(a => graph[a].To).Where(e => !e.IsFixed && (e.Pair != null) == pairedOnly).LastOrDefault();
                        if (opt["explain"])
                        {
                            Console.WriteLine("!!!!!!!!!!! Picking any edge whatsoever");
                        }
                        if (victimEdge == null)
                        {
                            throw new Exception($"No swappable edge found to inaccessible areas. This can happen a lot with low # of randomized entrances.");
                        }
                    }
                }
                if (opt["explain"])
                {
                    Console.WriteLine($"Swap unreached: {toFind}");
                    Console.WriteLine($"Swap redundant: {victimEdge}");
                }

                // Swap thos edges
                g.SwapConnectedEdges(victimEdge, toFind);
                pairedOnly = !opt["unconnected"];
            }
            if (check == null || check.Unvisited.Count > 0)
            {
                throw new Exception($"Couldn't solve seed {opt.DisplaySeed} - try a different one");
            }

            // Check succeeded, time to calculate scale and dump info
            float max = check.Records.Values.Where(r => !r.Area.StartsWith("kiln")).Select(r => r.Dist).Max();

            Dictionary <string, float> getCumCost(Dictionary <string, float> d)
            {
                Dictionary <string, float> total = new Dictionary <string, float>();
                float cost = 0;

                foreach (KeyValuePair <string, float> entry in d)
                {
                    cost            += entry.Value;
                    total[entry.Key] = cost;
                }
                return(total);
            }

            float getAreaCost(float dist)
            {
                float ratio = Math.Min(dist / max, 1);

                // return (float)Math.Pow(ratio, 0.75);
                return(ratio);
            }

            // TODO: Should sqrt also be used in DS1? ... it seems a bit weird, but the curves and area sizes are rather different
            Dictionary <string, float> distances = check.Records.Values.OrderBy(r => r.Dist).ToDictionary(r => r.Area, r => getAreaCost(r.Dist));
            Dictionary <string, float> thisDist  = getCumCost(distances);
            Dictionary <string, float> vCost     = getCumCost(ann.DefaultCost);
            List <float> vCosts = ann.DefaultCost.Select(t => t.Value).OrderBy(t => t).ToList();
            List <float> ratios = new List <float>();

            string maybeName(string area) => g.Areas.TryGetValue(area, out Area a) ? (a.Text ?? area) : area;

            // Choose one blacksmith.
            // If any paths have <=4 areas, choose them
            // If any paths have no bosses unique to that path, choose them
            // Otherwise, choose shortest?
            bool          ds1          = g.Areas.ContainsKey("asylum");
            List <string> upgradeAreas = ds1
                ? new List <string> {
                "parish_andre", "catacombs", "anorlondo_blacksmith"
            }                                                                               // "newlondo" doesn't sell titanite shards... although not that it matters with item rando
                : new List <string> {
                "firelink"
            };
            List <NodeRecord> upgradeNodes = upgradeAreas.Select(area => check.Records[area]).OrderBy(r => r.Visited.Count).ToList();
            NodeRecord        firstUpgrade;

            if (upgradeNodes[0].Visited.Count < 5)
            {
                firstUpgrade = upgradeNodes[0];
            }
            else
            {
                HashSet <string> commonAreas = new HashSet <string>(g.Areas.Keys);
                foreach (NodeRecord rec in upgradeNodes)
                {
                    commonAreas.IntersectWith(rec.Visited);
                }
                NodeRecord minBoss = upgradeNodes.Find(rec => commonAreas.IsSupersetOf(rec.Visited.Where(a => g.Areas[a].HasTag("boss"))));
                firstUpgrade = minBoss == null ? upgradeNodes[0] : minBoss;
            }
            List <string> preUpgrade = firstUpgrade.Visited;

            if (!opt["skipprint"])
            {
                Console.WriteLine($"Areas required before {maybeName(firstUpgrade.Area)}: {string.Join("; ", preUpgrade.Select(maybeName))}");
                Console.WriteLine($"Other areas are not necessary to get there.");
                Console.WriteLine();
            }

            foreach (string area in upgradeAreas)
            {
                if (opt["explain"])
                {
                    Console.WriteLine($"Blacksmith {area}: {string.Join(", ", check.Records[area].Visited)}");
                }
            }
            g.AreaRatios = new Dictionary <string, (float, float)>();
            int k = 0;

            float getRatioMeasure(float cost, float maxRatio)
            {
                return(1 + (maxRatio - 1) * cost);
            }

            foreach (NodeRecord rec in check.Records.Values.OrderBy(r => r.Dist))
            {
                float desiredCost = k < vCosts.Count ? vCosts[k] : 1;
                if (!g.Areas[rec.Area].HasTag("optional"))
                {
                    k++;
                }
                bool isBoss            = g.Areas[rec.Area].HasTag("boss");
                bool preBlacksmithBoss = preUpgrade.Contains(rec.Area) && isBoss;
                if (preBlacksmithBoss && desiredCost > 0.05)
                {
                    desiredCost = 0.05f;
                }
                float ratio    = 1;
                float dmgRatio = 1;
                if (g.Areas[rec.Area].HasTag("end"))
                {
                    // Keep ratio 1
                }
                else if (ann.DefaultCost.TryGetValue(rec.Area, out float defaultCost))
                {
                    // This scaling constant factor is a bit tricky to tune.
                    // Originally used 400-1100, based on HP scaling over the course of a game. This seems to better match expected boss HP.
                    // Possible ratio range: 0.3 to 3 in DS1
                    ratio = getRatioMeasure(desiredCost, ann.HealthScaling) / getRatioMeasure(defaultCost, ann.HealthScaling);
                    // If it's randomized to past 70% of the way, don't make it easier.
                    if (ratio < 1 && ((double)k / check.Records.Count) > 0.7)
                    {
                        ratio = 1;
                    }
                    // If it's early enough in vanilla (i.e. before expected access to blacksmith in DS1), don't make it easier either.
                    else if (defaultCost <= (ann.DefaultCost.TryGetValue(ds1 ? "parish_church" : "settlement", out float val) ? val : 0.25) && ratio < 1)
                    {
                        ratio = 1;
                    }
                    else
                    {
                        // Damage does not scale as much
                        // Possible ratio range: 0.5 to 2 in DS1
                        dmgRatio = getRatioMeasure(desiredCost, ann.DamageScaling) / getRatioMeasure(defaultCost, ann.DamageScaling);
                    }
                }
                g.AreaRatios[rec.Area] = (ratio, dmgRatio);

                if (opt["skipprint"])
                {
                    continue;
                }
                // Print out the connectivity info for spoiler logs
                if (rec.Area == (ds1 ? "anorlondo_os" : "firelink"))
                {
                    Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
                }
                string areas       = opt["debugareas"] ? $" [{string.Join(",", new SortedSet<string>(rec.Visited))}]" : "";
                string scaling     = opt["scale"] ? $" (scaling: {ratio * 100:0.}%)" : "";
                string explainCost = opt["explain"] ? $" {desiredCost * 100:0.}%" : "";

                Console.WriteLine($"{maybeName(rec.Area)}{explainCost}" + scaling + areas + (isBoss ? " <----" : ""));
                foreach (KeyValuePair <Edge, float> entry in rec.InEdge.OrderBy(e => e.Value))
                {
                    Edge          e         = entry.Key;
                    List <string> itemAreas = e.LinkedExpr == null
                        ? new List <string>()
                        : e.LinkedExpr.FreeVars().SelectMany(a => g.ItemAreas.TryGetValue(a, out List <string> deps) ? deps: new List <string>()).Distinct().ToList();
                    string itemDeps = itemAreas.Count == 0 ? "" : $", an item from {string.Join(" and ", itemAreas.Select(a => maybeName(a)))}";

                    // Don't print entry.Value directly (distance of edge) - hard to visualize
                    if (e.Text == e.Link.Text)
                    {
                        Console.WriteLine($"  Preexisting: From {maybeName(e.From)} to {maybeName(rec.Area)} ({e.Text}{itemDeps})");
                    }
                    else
                    {
                        Console.WriteLine($"  Random: From {maybeName(e.From)} ({e.Text}) to {maybeName(rec.Area)} ({e.Link.Text}{itemDeps})");
                    }
                }
            }
            if (opt["dumpdist"])
            {
                foreach (KeyValuePair <string, float> entry in distances)
                {
                    Area area = g.Areas[entry.Key];
                    if (area.HasTag("optional"))
                    {
                        continue;
                    }
                    Console.WriteLine($"{entry.Key}: {entry.Value}  # SL {(int)(10 + (ds1 ? 60 : 70) * entry.Value)}");
                }
            }
            Console.WriteLine($"Finished {opt.DisplaySeed} at try {tries}");
            if (opt["explain"])
            {
                Console.WriteLine($"Pre-Blacksmith areas ({firstUpgrade.Area}): {string.Join(", ", preUpgrade)}");
            }

            if (opt["dumpgraph"])
            {
                Console.WriteLine("Writing ../fog.dot");
                bool       bi  = false;
                TextWriter dot = File.CreateText(@"..\fog.dot");
                dot.WriteLine($"{(bi ? "di" : "")}graph {{");
                // dot.WriteLine("  nodesep=0.1; ranksep=0.1; ");
                string escape(object o)
                {
                    if (o == null)
                    {
                        return("");
                    }
                    return(o.ToString().Replace("\n", "\\l").Replace("\"", "\\\"") + "\\l");
                }

                foreach (Node node in graph.Values)
                {
                    string label = node.Area;
                    label = label == "" ? "(empty)" : label;
                    dot.WriteLine($"    \"{node.Area}\" [ shape=box,label=\"{escape(label)}\" ];");
                }
                HashSet <Connection> oneCons = new HashSet <Connection>();
                foreach (Node from in graph.Values)
                {
                    foreach (Edge e in from.To)
                    {
                        Connection con = new Connection(e.From, e.To);
                        if (oneCons.Contains(con))
                        {
                            continue;
                        }
                        if (!bi)
                        {
                            oneCons.Add(con);
                        }
                        // Node to = e.To;
                        string toKey = e.To;
                        string style = "solid";
                        string label = null; // $"{e.LinkedExpr}";
                        dot.WriteLine($"  \"{from.Area}\" -{(bi ? ">" : "-")} \"{toKey}\" [ style={style},labelloc=t,label=\"{escape(label)}\" ];");
                    }
                }
                dot.WriteLine("}");
                dot.Close();
            }
        }
Example #3
0
        public void WriteEventConfig(AnnotationData ann, Events events, RandomizerOptions opt)
        {
            GameEditor editor = new GameEditor(FromGame.DS3);

            editor.Spec.GameDir = "fogdist";
            Dictionary <string, MSB3>  maps   = editor.Load(@"Base", path => ann.Specs.ContainsKey(GameEditor.BaseName(path)) ? MSB3.Read(path) : null, "*.msb.dcx");
            Dictionary <string, EMEVD> emevds = editor.Load(@"Base", path => ann.Specs.ContainsKey(GameEditor.BaseName(path)) || path.Contains("common") ? EMEVD.Read(path) : null, "*.emevd.dcx");

            void deleteEmpty <K, V>(Dictionary <K, V> d)
            {
                foreach (K key in d.Keys.ToList())
                {
                    if (d[key] == null)
                    {
                        d.Remove(key);
                    }
                }
            }

            // Should this be in GameEditor?
            deleteEmpty(maps);
            deleteEmpty(emevds);

            editor.Spec.NameDir = @"fogdist\Names";
            Dictionary <string, string>    modelNames = editor.LoadNames("ModelName", n => n);
            SortedDictionary <int, string> chars      = new SortedDictionary <int, string>(editor.LoadNames("CharaInitParam", n => int.Parse(n)));

            Dictionary <string, List <string> > description          = new Dictionary <string, List <string> >();
            Dictionary <int, string>            entityNames          = new Dictionary <int, string>();
            Dictionary <int, List <int> >       groupIds             = new Dictionary <int, List <int> >();
            Dictionary <(string, string), MSB3.Event.ObjAct> objacts = new Dictionary <(string, string), MSB3.Event.ObjAct>();

            HashSet <int> highlightIds = new HashSet <int>();
            HashSet <int> selectIds    = new HashSet <int>();

            foreach (Entrance e in ann.Warps.Concat(ann.Entrances))
            {
                int id = e.ID;
                AddMulti(description, id.ToString(), (ann.Warps.Contains(e) ? "" : "fog gate ") + e.Text);
                selectIds.Add(e.ID);
                highlightIds.Add(e.ID);
            }
            HashSet <string> gameObjs = new HashSet <string>();

            foreach (GameObject obj in ann.Objects)
            {
                if (int.TryParse(obj.ID, out int id))
                {
                    AddMulti(description, id.ToString(), obj.Text);
                    selectIds.Add(id);
                    highlightIds.Add(id);
                }
                else
                {
                    gameObjs.Add($"{obj.Area}_{obj.ID}");
                }
            }

            Dictionary <string, Dictionary <string, FMG> > fmgs = new GameEditor(FromGame.DS3).LoadBnds($@"msg\engus", (data, name) => FMG.Read(data), ext: "*_dlc2.msgbnd.dcx");

            void addFMG(FMG fmg, string desc)
            {
                foreach (FMG.Entry e in fmg.Entries)
                {
                    if (e.ID > 25000 && !string.IsNullOrWhiteSpace(e.Text))
                    {
                        highlightIds.Add(e.ID);
                        AddMulti(description, e.ID.ToString(), desc + " " + "\"" + e.Text.Replace("\r", "").Replace("\n", "\\n") + "\"");
                    }
                }
            }

            addFMG(fmgs["item_dlc2"]["NPC名"], "name");
            addFMG(fmgs["menu_dlc2"]["イベントテキスト"], "text");

            foreach (KeyValuePair <string, MSB3> entry in maps)
            {
                string map = ann.Specs[entry.Key].Name;
                MSB3   msb = entry.Value;

                foreach (MSB3.Part e in msb.Parts.GetEntries())
                {
                    string shortName = $"{map}_{e.Name}";
                    if (modelNames.TryGetValue(e.ModelName, out string modelDesc))
                    {
                        if (e is MSB3.Part.Enemy en && modelDesc == "Human NPC" && en.CharaInitID > 0)
                        {
                            modelDesc = CharacterName(chars, en.CharaInitID);
                        }
                        else if (e is MSB3.Part.Player)
                        {
                            modelDesc = "Warp Point";
                        }
                        AddMulti(description, shortName, modelDesc);
                    }
                    AddMulti(description, shortName, $"{map} {e.GetType().Name.ToString().ToLowerInvariant()} {e.Name}");  // {(e.EventEntityID > 0 ? $" {e.EventEntityID}" : "")}
                    if (e.EventEntityID > 10)
                    {
                        highlightIds.Add(e.EventEntityID);
                        string idStr = e.EventEntityID.ToString();
                        if (description.ContainsKey(idStr))
                        {
                            AddMulti(description, shortName, description[idStr]);
                        }
                        description[idStr] = description[shortName];
                        if (e is MSB3.Part.Player || e.ModelName == "o000100")
                        {
                            selectIds.Add(e.EventEntityID);
                        }
                        if (selectIds.Contains(e.EventEntityID))
                        {
                            gameObjs.Add(shortName);
                        }

                        foreach (int id in e.EventEntityGroups)
                        {
                            if (id > 0)
                            {
                                AddMulti(groupIds, id, e.EventEntityID);
                                highlightIds.Add(id);
                            }
                        }
                    }
                }
                foreach (MSB3.Region r in msb.Regions.GetEntries())
                {
                    if (r.EventEntityID < 1000000)
                    {
                        continue;
                    }
                    AddMulti(description, r.EventEntityID.ToString(), $"{map} {r.GetType().Name.ToLowerInvariant()} region {r.Name}");
                    highlightIds.Add(r.EventEntityID);
                }
                foreach (MSB3.Event e in msb.Events.GetEntries())
                {
                    if (e is MSB3.Event.ObjAct oa)
                    {
                        // It can be null, basically for commented out objacts
                        string part = oa.PartName ?? oa.PartName2;
                        if (part == null)
                        {
                            continue;
                        }
                        string desc = description.TryGetValue($"{map}_{part}", out List <string> p) ? string.Join(" - ", p) : throw new Exception($"{map} {oa.Name}");
                        objacts[(map, part)] = oa;