예제 #1
0
 public StaticEdgeReversed(StaticEdge Child, StaticNode target)
 {
     this.Child      = Child;
     this.NodeTarget = target;
     this.FromNode   = Child.FromNode;
 }
예제 #2
0
        private void ProcessSubroom(StaticNode node, RandoConfigRoom config)
        {
            if (node.Name != "main" && config.Tweaks != null)
            {
                throw new Exception("Config error: you have a subroom with tweaks in it");
            }

            foreach (RandoConfigHole holeConfig in config.Holes ?? new List <RandoConfigHole>())
            {
                Hole matchedHole      = null;
                int  remainingMatches = holeConfig.Idx;
                foreach (Hole hole in this.Holes)
                {
                    if (hole.Side == holeConfig.Side)
                    {
                        if (remainingMatches == 0)
                        {
                            matchedHole = hole;
                            break;
                        }
                        else
                        {
                            remainingMatches--;
                        }
                    }
                }

                if (matchedHole == null)
                {
                    throw new Exception($"Could not find the hole identified by area:{this.Area} room:{config.Room} side:{holeConfig.Side} idx:{holeConfig.Idx}");
                }

                //Logger.Log("randomizer", $"Matching {roomConfig.Room} {holeConfig.Side} {holeConfig.Idx} to {matchedHole}");
                matchedHole.Kind   = holeConfig.Kind;
                matchedHole.Launch = holeConfig.Launch;
                if (holeConfig.LowBound != null)
                {
                    matchedHole.LowBound = (int)holeConfig.LowBound;
                }
                if (holeConfig.HighBound != null)
                {
                    matchedHole.HighBound = (int)holeConfig.HighBound;
                }
                if (holeConfig.HighOpen != null)
                {
                    matchedHole.HighOpen = (bool)holeConfig.HighOpen;
                }

                if (holeConfig.Kind != HoleKind.None)
                {
                    node.Edges.Add(new StaticEdge()
                    {
                        FromNode   = node,
                        HoleTarget = matchedHole,
                        ReqIn      = this.ProcessReqs(holeConfig.ReqIn, matchedHole, false),
                        ReqOut     = this.ProcessReqs(holeConfig.ReqOut, matchedHole, true)
                    });
                }
            }

            foreach (var edge in config.InternalEdges ?? new List <RandoConfigInternalEdge>())
            {
                StaticNode toNode;
                if (edge.To != null)
                {
                    toNode = this.Nodes[edge.To];
                }
                else if (edge.Split != null)
                {
                    if (node.Edges.Count != 2)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Cannot split: must have exactly two edges");
                    }

                    toNode = new StaticNode()
                    {
                        Name       = node.Name + "_autosplit",
                        ParentRoom = node.ParentRoom
                    };
                    if (node.ParentRoom.Nodes.ContainsKey(toNode.Name))
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once");
                    }
                    node.ParentRoom.Nodes[toNode.Name] = toNode;

                    bool firstMain;
                    var  first  = node.Edges[0].HoleTarget;
                    var  second = node.Edges[1].HoleTarget;
                    switch (edge.Split)
                    {
                    case RandoConfigInternalEdge.SplitKind.BottomToTop:
                        firstMain = first.Side == ScreenDirection.Down || second.Side == ScreenDirection.Up ||
                                    (first.Side != ScreenDirection.Up && second.Side != ScreenDirection.Down && first.HighBound > second.HighBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.TopToBottom:
                        firstMain = first.Side == ScreenDirection.Up || second.Side == ScreenDirection.Down ||
                                    (first.Side != ScreenDirection.Down && second.Side != ScreenDirection.Up && first.LowBound < second.LowBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.RightToLeft:
                        firstMain = first.Side == ScreenDirection.Right || second.Side == ScreenDirection.Left ||
                                    (first.Side != ScreenDirection.Left && second.Side != ScreenDirection.Right && first.HighBound > second.HighBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.LeftToRight:
                    default:
                        firstMain = first.Side == ScreenDirection.Left || second.Side == ScreenDirection.Right ||
                                    (first.Side != ScreenDirection.Right && second.Side != ScreenDirection.Left && first.LowBound < second.LowBound);
                        break;
                    }

                    var secondary = firstMain ? node.Edges[1] : node.Edges[0];
                    node.Edges.Remove(secondary);
                    toNode.Edges.Add(secondary);
                    secondary.FromNode = toNode;
                }
                else if (edge.Collectable != null)
                {
                    toNode = new StaticNode()
                    {
                        Name       = node.Name + "_coll" + edge.Collectable.ToString(),
                        ParentRoom = node.ParentRoom
                    };
                    if (node.ParentRoom.Nodes.ContainsKey(toNode.Name))
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once");
                    }
                    node.ParentRoom.Nodes[toNode.Name] = toNode;

                    var thing = this.Collectables[edge.Collectable.Value];
                    if (thing.ParentNode != null)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner");
                    }
                    thing.ParentNode = toNode;
                    toNode.Collectables.Add(thing);
                }
                else
                {
                    throw new Exception($"[{this.Name}.{node.Name}] Internal edge must have either To or Split or Collectable");
                }

                var reqIn  = this.ProcessReqs(edge.ReqIn, null, false);
                var reqOut = this.ProcessReqs(edge.ReqOut, null, true);

                var forward = new StaticEdge()
                {
                    FromNode   = node,
                    NodeTarget = toNode,
                    ReqIn      = reqIn,
                    ReqOut     = reqOut
                };
                var reverse = new StaticEdgeReversed(forward, node);

                node.Edges.Add(forward);
                toNode.Edges.Add(reverse);
            }

            foreach (var col in config.Collectables)
            {
                if (col.Idx != null)
                {
                    var thing = this.Collectables[col.Idx.Value];
                    if (thing.ParentNode != null)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner");
                    }
                    thing.ParentNode = node;
                    thing.MustFly    = col.MustFly;
                    node.Collectables.Add(thing);
                }
                else if (col.X != null && col.Y != null)
                {
                    node.Collectables.Add(new StaticCollectable {
                        ParentNode = node,
                        Position   = new Vector2((float)col.X.Value, (float)col.Y.Value),
                        MustFly    = col.MustFly
                    });
                }
                else
                {
                    throw new Exception($"[{this.Name}.{node.Name}] Collectable must specify Idx or X/Y");
                }
            }
        }
예제 #3
0
        public StaticRoom(AreaKey Area, RandoConfigRoom config, LevelData Level, List <Hole> Holes)
        {
            // hack: force credits screens into the epilogue roomset
            if (Area.ID == 7 && Level.Name.StartsWith("credits-"))
            {
                Area = new AreaKey(8);
            }
            this.Area  = Area;
            this.Level = Level;
            this.Holes = Holes;

            this.Name            = AreaData.Get(Area).GetSID() + "/" + (Area.Mode == AreaMode.Normal ? "A" : Area.Mode == AreaMode.BSide ? "B" : "C") + "/" + Level.Name;
            this.ReqEnd          = this.ProcessReqs(config.ReqEnd);
            this.Hub             = config.Hub;
            this.Tweaks          = config.Tweaks ?? new List <RandoConfigEdit>();
            this.CoreModes       = config.Core;
            this.ExtraSpace      = config.ExtraSpace ?? new List <RandoConfigRectangle>();
            this.Worth           = config.Worth ?? (float)Math.Sqrt(Level.Bounds.Width * Level.Bounds.Width + Level.Bounds.Height * Level.Bounds.Height) / 369.12870384189847f + 1;
            this.SpinnersShatter = config.SpinnersShatter;

            this.Collectables = new List <StaticCollectable>();
            foreach (var entity in Level.Entities)
            {
                if (RandoModule.Instance.MetaConfig.CollectableNames.Contains(entity.Name))
                {
                    this.Collectables.Add(new StaticCollectable {
                        Position = entity.Position,
                        MustFly  = false,
                    });
                }
            }
            this.Collectables.Sort((a, b) => {
                if (a.Position.Y > b.Position.Y)
                {
                    return(1);
                }
                else if (a.Position.Y < b.Position.Y)
                {
                    return(-1);
                }
                else if (a.Position.X > b.Position.X)
                {
                    return(1);
                }
                else if (a.Position.X < b.Position.X)
                {
                    return(-1);
                }
                else
                {
                    return(0);
                }
            });

            this.Nodes = new Dictionary <string, StaticNode>()
            {
                { "main", new StaticNode()
                  {
                      Name       = "main",
                      ParentRoom = this
                  } }
            };
            foreach (var subroom in config.Subrooms ?? new List <RandoConfigRoom>())
            {
                if (subroom.Room == null || this.Nodes.ContainsKey(subroom.Room))
                {
                    throw new Exception($"Invalid subroom name in {this.Area} {this.Name}");
                }
                this.Nodes.Add(subroom.Room, new StaticNode()
                {
                    Name = subroom.Room, ParentRoom = this
                });
            }

            this.ProcessSubroom(this.Nodes["main"], config);
            foreach (var subroom in config.Subrooms ?? new List <RandoConfigRoom>())
            {
                this.ProcessSubroom(this.Nodes[subroom.Room], subroom);
            }

            // assign unmarked holes
            foreach (var uhole in this.Holes)
            {
                if (uhole.Kind != HoleKind.Unknown)
                {
                    continue;
                }

                var        bestDist = 10000f;
                StaticNode bestNode = null;
                var        lowPos   = uhole.LowCoord(this.Level.Bounds);
                var        highPos  = uhole.HighCoord(this.Level.Bounds);
                foreach (var node in this.Nodes.Values)
                {
                    foreach (var edge in node.Edges.Where(edge => edge.HoleTarget != null))
                    {
                        if (edge.HoleTarget == uhole)
                        {
                            bestNode = null;
                            goto doublebreak;
                        }

                        var pos  = edge.HoleTarget.LowCoord(this.Level.Bounds);
                        var dist = (pos - lowPos).Length();
                        if (!uhole.LowOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        dist = (pos - highPos).Length();
                        if (!uhole.HighOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        pos  = edge.HoleTarget.HighCoord(this.Level.Bounds);
                        dist = (pos - lowPos).Length();
                        if (!uhole.LowOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        dist = (pos - highPos).Length();
                        if (!uhole.HighOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }
                    }
                }

doublebreak:
                bestNode?.Edges?.Add(new StaticEdge {
                    FromNode   = bestNode,
                    HoleTarget = uhole,
                    ReqIn      = this.ProcessReqs(null, uhole, false),
                    ReqOut     = this.ProcessReqs(null, uhole, true),
                });
            }

            // assign unmarked collectables
            foreach (var c in this.Collectables)
            {
                if (c.ParentNode != null)
                {
                    continue;
                }

                var        bestDist = 1000f;
                StaticNode bestNode = null;
                foreach (var node in this.Nodes.Values)
                {
                    foreach (var edge in node.Edges)
                    {
                        if (edge.HoleTarget == null)
                        {
                            continue;
                        }

                        var pos  = edge.HoleTarget.LowCoord(new Rectangle(0, 0, this.Level.Bounds.Width, this.Level.Bounds.Height));
                        var dist = (pos - c.Position).Length();
                        if (dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        pos  = edge.HoleTarget.HighCoord(new Rectangle(0, 0, this.Level.Bounds.Width, this.Level.Bounds.Height));
                        dist = (pos - c.Position).Length();
                        if (dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }
                    }
                }

                if (bestNode != null)
                {
                    c.ParentNode = bestNode;
                    bestNode.Collectables.Add(c);
                }
            }

            // perform fg tweaks
            var regex     = new Regex("\\r\\n|\\n\\r|\\n|\\r");
            var tweakable = new List <List <char> >();

            foreach (var line in regex.Split(Level.Solids))
            {
                var lst = new List <char>();
                tweakable.Add(lst);
                foreach (var ch in line)
                {
                    lst.Add(ch);
                }
            }

            void setTile(int x, int y, char tile)
            {
                while (y >= tweakable.Count)
                {
                    tweakable.Add(new List <char>());
                }

                while (x >= tweakable[y].Count)
                {
                    tweakable[y].Add('0');
                }

                tweakable[y][x] = tile;
            }

            foreach (var tweak in config.Tweaks ?? new List <RandoConfigEdit>())
            {
                if (tweak.Name == "fgTiles")
                {
                    setTile((int)tweak.X, (int)tweak.Y, tweak.Update.Tile);
                }
            }

            Level.Solids = string.Join("\n", tweakable.Select(line => string.Join("", line)));

            // peform decal tweaks
            foreach (var decalList in new[] { Level.FgDecals, Level.BgDecals })
            {
                var removals = new List <DecalData>();
                foreach (var decal in decalList)
                {
                    var fg = object.ReferenceEquals(decalList, Level.FgDecals);
                    foreach (var tweak in config.Tweaks ?? new List <RandoConfigEdit>())
                    {
                        if (tweak.Decal == (fg ? RandoConfigDecalType.FG : RandoConfigDecalType.BG) &&
                            (tweak.Name == null || tweak.Name == decal.Texture) &&
                            (tweak.X == null || (int)tweak.X.Value == (int)decal.Position.X) &&
                            (tweak.Y == null || (int)tweak.Y.Value == (int)decal.Position.Y))
                        {
                            if (tweak.Update?.Remove ?? false)
                            {
                                removals.Add(decal);
                            }
                            else
                            {
                                if (tweak.Update?.X != null)
                                {
                                    decal.Position.X = tweak.Update.X.Value;
                                }
                                if (tweak.Update?.Y != null)
                                {
                                    decal.Position.Y = tweak.Update.Y.Value;
                                }
                                if (tweak.Update?.ScaleX != null)
                                {
                                    decal.Position.X = tweak.Update.ScaleX.Value;
                                }
                                if (tweak.Update?.ScaleY != null)
                                {
                                    decal.Position.Y = tweak.Update.ScaleY.Value;
                                }
                            }

                            break;
                        }
                    }
                }

                foreach (var decal in removals)
                {
                    decalList.Remove(decal);
                }
            }

            foreach (var tweak in config.Tweaks ?? new List <RandoConfigEdit>())
            {
                if ((tweak.Update?.Add ?? false) && tweak.Decal != RandoConfigDecalType.None)
                {
                    var newDecal = new DecalData {
                        Texture  = tweak.Name,
                        Position = new Vector2(tweak.Update.X.Value, tweak.Update.Y.Value),
                        Scale    = new Vector2(tweak.Update.ScaleX.Value, tweak.Update.ScaleY.Value),
                    };
                    (tweak.Decal == RandoConfigDecalType.BG ? Level.BgDecals : Level.FgDecals).Add(newDecal);
                }
            }
        }
예제 #4
0
        public StaticRoom(AreaKey Area, RandoConfigRoom config, LevelData Level, List <Hole> Holes)
        {
            this.Area  = Area;
            this.Level = Level;
            this.Holes = Holes;

            this.Name       = AreaData.Get(Area).GetSID() + "/" + (Area.Mode == AreaMode.Normal ? "A" : Area.Mode == AreaMode.BSide ? "B" : "C") + "/" + Level.Name;
            this.End        = config.End;
            this.Hub        = config.Hub;
            this.Tweaks     = config.Tweaks ?? new List <RandoConfigEdit>();
            this.CoreModes  = config.Core;
            this.ExtraSpace = config.ExtraSpace ?? new List <RandoConfigRectangle>();

            this.Collectables = new List <StaticCollectable>();
            foreach (var entity in Level.Entities)
            {
                switch (entity.Name.ToLower())
                {
                case "strawberry":
                case "key":
                    this.Collectables.Add(new StaticCollectable {
                        Position = entity.Position,
                        MustFly  = false,
                    });
                    break;
                }
            }
            this.Collectables.Sort((StaticCollectable a, StaticCollectable b) => {
                if (a.Position.Y > b.Position.Y)
                {
                    return(1);
                }
                else if (a.Position.Y < b.Position.Y)
                {
                    return(-1);
                }
                else if (a.Position.X > b.Position.X)
                {
                    return(1);
                }
                else if (a.Position.X < b.Position.X)
                {
                    return(-1);
                }
                else
                {
                    return(0);
                }
            });

            this.Nodes = new Dictionary <string, StaticNode>()
            {
                { "main", new StaticNode()
                  {
                      Name       = "main",
                      ParentRoom = this
                  } }
            };
            foreach (var subroom in config.Subrooms ?? new List <RandoConfigRoom>())
            {
                if (subroom.Room == null || this.Nodes.ContainsKey(subroom.Room))
                {
                    throw new Exception($"Invalid subroom name in {this.Area} {this.Name}");
                }
                this.Nodes.Add(subroom.Room, new StaticNode()
                {
                    Name = subroom.Room, ParentRoom = this
                });
            }

            this.ProcessSubroom(this.Nodes["main"], config);
            foreach (var subroom in config.Subrooms ?? new List <RandoConfigRoom>())
            {
                this.ProcessSubroom(this.Nodes[subroom.Room], subroom);
            }

            // assign unmarked holes
            foreach (var uhole in this.Holes)
            {
                if (uhole.Kind != HoleKind.Unknown)
                {
                    continue;
                }

                var        bestDist = 10000f;
                StaticNode bestNode = null;
                var        lowPos   = uhole.LowCoord(this.Level.Bounds);
                var        highPos  = uhole.HighCoord(this.Level.Bounds);
                foreach (var node in this.Nodes.Values)
                {
                    foreach (var edge in node.Edges)
                    {
                        if (edge.HoleTarget == null)
                        {
                            continue;
                        }
                        if (edge.HoleTarget == uhole)
                        {
                            bestNode = null;
                            goto doublebreak;
                        }

                        var pos  = edge.HoleTarget.LowCoord(this.Level.Bounds);
                        var dist = (pos - lowPos).Length();
                        if (!uhole.LowOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        dist = (pos - highPos).Length();
                        if (!uhole.HighOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        pos  = edge.HoleTarget.HighCoord(this.Level.Bounds);
                        dist = (pos - lowPos).Length();
                        if (!uhole.LowOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        dist = (pos - highPos).Length();
                        if (!uhole.HighOpen && dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }
                    }
                }

doublebreak:
                if (bestNode != null)
                {
                    bestNode.Edges.Add(new StaticEdge {
                        FromNode   = bestNode,
                        HoleTarget = uhole,
                        ReqIn      = this.ProcessReqs(null, uhole, false),
                        ReqOut     = this.ProcessReqs(null, uhole, true),
                    });
                }
            }

            // assign unmarked collectables
            foreach (var c in this.Collectables)
            {
                if (c.ParentNode != null)
                {
                    continue;
                }

                var        bestDist = 1000f;
                StaticNode bestNode = null;
                foreach (var node in this.Nodes.Values)
                {
                    foreach (var edge in node.Edges)
                    {
                        if (edge.HoleTarget == null)
                        {
                            continue;
                        }

                        var pos  = edge.HoleTarget.LowCoord(new Rectangle(0, 0, this.Level.Bounds.Width, this.Level.Bounds.Height));
                        var dist = (pos - c.Position).Length();
                        if (dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }

                        pos  = edge.HoleTarget.HighCoord(new Rectangle(0, 0, this.Level.Bounds.Width, this.Level.Bounds.Height));
                        dist = (pos - c.Position).Length();
                        if (dist < bestDist)
                        {
                            bestDist = dist;
                            bestNode = node;
                        }
                    }
                }

                if (bestNode != null)
                {
                    c.ParentNode = bestNode;
                    bestNode.Collectables.Add(c);
                }
            }
        }
예제 #5
0
        private void ProcessSubroom(StaticNode node, RandoConfigRoom config)
        {
            if (node.Name != "main" && config.Tweaks != null)
            {
                throw new Exception("Config error: you have a subroom with tweaks in it");
            }

            foreach (RandoConfigHole holeConfig in config.Holes ?? new List <RandoConfigHole>())
            {
                if (holeConfig.New)
                {
                    if (holeConfig.LowBound == null || holeConfig.HighBound == null)
                    {
                        throw new Exception("Config error: new hole missing LowBound/HighBound");
                    }

                    if (holeConfig.Kind == HoleKind.None)
                    {
                        throw new Exception("You probably didn't mean to add a new hole with kind None");
                    }

                    var hole = new Hole(holeConfig.Side, holeConfig.LowBound.Value, holeConfig.HighBound.Value, holeConfig.HighOpen ?? false)
                    {
                        Launch = holeConfig.Launch,
                        Kind   = holeConfig.Kind,
                    };
                    this.Holes.Add(hole);
                    node.Edges.Add(new StaticEdge {
                        FromNode   = node,
                        HoleTarget = hole,
                        ReqIn      = this.ProcessReqs(holeConfig.ReqIn, hole, false),
                        ReqOut     = this.ProcessReqs(holeConfig.ReqOut, hole, true),
                    });
                    continue;
                }
                Hole matchedHole      = null;
                var  remainingMatches = holeConfig.Idx;
                foreach (var hole in this.Holes.Where(hole => hole.Side == holeConfig.Side))
                {
                    if (remainingMatches == 0)
                    {
                        matchedHole = hole;
                        break;
                    }
                    remainingMatches--;
                }

                if (matchedHole == null)
                {
                    throw new Exception($"Could not find the hole identified by area:{this.Area} room:{config.Room} side:{holeConfig.Side} idx:{holeConfig.Idx}");
                }

                //Logger.Log("randomizer", $"Matching {roomConfig.Room} {holeConfig.Side} {holeConfig.Idx} to {matchedHole}");
                matchedHole.Kind   = holeConfig.Kind;
                matchedHole.Launch = holeConfig.Launch;
                if (holeConfig.LowBound != null)
                {
                    matchedHole.LowBound = holeConfig.LowBound.Value;
                }
                if (holeConfig.HighBound != null)
                {
                    matchedHole.HighBound = holeConfig.HighBound.Value;
                }
                if (holeConfig.HighOpen != null)
                {
                    matchedHole.HighOpen = holeConfig.HighOpen.Value;
                }

                if (holeConfig.Kind != HoleKind.None)
                {
                    node.Edges.Add(new StaticEdge {
                        FromNode   = node,
                        HoleTarget = matchedHole,
                        ReqIn      = this.ProcessReqs(holeConfig.ReqIn, matchedHole, false),
                        ReqOut     = this.ProcessReqs(holeConfig.ReqOut, matchedHole, true),
                    });
                }

                if (holeConfig.Split != null)
                {
                    matchedHole.HighOpen = true;

                    var hole = new Hole(matchedHole.Side, 0, matchedHole.HighBound, false)
                    {
                        Launch = holeConfig.Split.Launch,
                        Kind   = holeConfig.Split.Kind,
                    };
                    this.Holes.Add(hole);
                    node.Edges.Add(new StaticEdge {
                        FromNode   = node,
                        HoleTarget = hole,
                        ReqIn      = this.ProcessReqs(holeConfig.Split.ReqIn, hole, false),
                        ReqOut     = this.ProcessReqs(holeConfig.Split.ReqOut, hole, true),
                    });
                }
            }

            foreach (var edge in config.InternalEdges ?? new List <RandoConfigInternalEdge>())
            {
                StaticNode toNode;
                if (edge.Warp != null)
                {
                    // deal with this later
                    node.WarpConfig.Add(edge);
                    continue;
                }
                else if (edge.To != null)
                {
                    if (!this.Nodes.ContainsKey(edge.To))
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] \"To\" edge says \"{edge.To}\" but no such subroom exists");
                    }
                    toNode = this.Nodes[edge.To];
                }
                else if (edge.CustomWarp)
                {
                    toNode = null;
                }
                else if (edge.Split != null)
                {
                    if (node.Edges.Count != 2)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Cannot split: must have exactly two edges");
                    }

                    toNode = new StaticNode()
                    {
                        Name       = node.Name + "_autosplit",
                        ParentRoom = node.ParentRoom
                    };
                    if (node.ParentRoom.Nodes.ContainsKey(toNode.Name))
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once");
                    }
                    node.ParentRoom.Nodes[toNode.Name] = toNode;

                    bool firstMain;
                    var  first  = node.Edges[0].HoleTarget;
                    var  second = node.Edges[1].HoleTarget;
                    switch (edge.Split)
                    {
                    case RandoConfigInternalEdge.SplitKind.BottomToTop:
                        firstMain = first.Side == ScreenDirection.Down || second.Side == ScreenDirection.Up ||
                                    (first.Side != ScreenDirection.Up && second.Side != ScreenDirection.Down && first.HighBound > second.HighBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.TopToBottom:
                        firstMain = first.Side == ScreenDirection.Up || second.Side == ScreenDirection.Down ||
                                    (first.Side != ScreenDirection.Down && second.Side != ScreenDirection.Up && first.LowBound < second.LowBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.RightToLeft:
                        firstMain = first.Side == ScreenDirection.Right || second.Side == ScreenDirection.Left ||
                                    (first.Side != ScreenDirection.Left && second.Side != ScreenDirection.Right && first.HighBound > second.HighBound);
                        break;

                    case RandoConfigInternalEdge.SplitKind.LeftToRight:
                    default:
                        firstMain = first.Side == ScreenDirection.Left || second.Side == ScreenDirection.Right ||
                                    (first.Side != ScreenDirection.Right && second.Side != ScreenDirection.Left && first.LowBound < second.LowBound);
                        break;
                    }

                    var secondary = firstMain ? node.Edges[1] : node.Edges[0];
                    node.Edges.Remove(secondary);
                    toNode.Edges.Add(secondary);
                    secondary.FromNode = toNode;
                }
                else if (edge.Collectable != null)
                {
                    toNode = new StaticNode()
                    {
                        Name       = node.Name + "_coll" + edge.Collectable.ToString(),
                        ParentRoom = node.ParentRoom
                    };
                    if (node.ParentRoom.Nodes.ContainsKey(toNode.Name))
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] You may only autosplit a room once");
                    }
                    node.ParentRoom.Nodes[toNode.Name] = toNode;

                    var thing = this.Collectables[edge.Collectable.Value];
                    if (thing.ParentNode != null)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner");
                    }
                    thing.ParentNode = toNode;
                    toNode.Collectables.Add(thing);
                }
                else
                {
                    throw new Exception($"[{this.Name}.{node.Name}] Internal edge must have either To or Split or Collectable or CustomWarp");
                }

                var reqIn  = this.ProcessReqs(edge.ReqIn, null, false);
                var reqOut = this.ProcessReqs(edge.ReqOut, null, true);

                var forward = new StaticEdge()
                {
                    FromNode   = node,
                    NodeTarget = toNode,
                    ReqIn      = reqIn,
                    ReqOut     = reqOut,
                    CustomWarp = edge.CustomWarp,
                };

                node.Edges.Add(forward);
                if (toNode != null)
                {
                    var reverse = new StaticEdgeReversed(forward, node);
                    toNode.Edges.Add(reverse);
                }
            }

            foreach (var col in config.Collectables)
            {
                if (col.Idx != null)
                {
                    var thing = this.Collectables[col.Idx.Value];
                    if (thing.ParentNode != null)
                    {
                        throw new Exception($"[{this.Name}.{node.Name}] Can only assign a collectable to one owner");
                    }
                    thing.ParentNode = node;
                    thing.MustFly    = col.MustFly;
                    node.Collectables.Add(thing);
                }
                else if (col.X != null && col.Y != null)
                {
                    node.Collectables.Add(new StaticCollectable {
                        ParentNode = node,
                        Position   = new Vector2(col.X.Value, col.Y.Value),
                        MustFly    = col.MustFly
                    });
                }
                else
                {
                    throw new Exception($"[{this.Name}.{node.Name}] Collectable must specify Idx or X/Y");
                }
            }

            foreach (var flagStr in config.Flags ?? new List <string>())
            {
                var name = flagStr;
                var val  = true;
                if (name.Contains(":"))
                {
                    var split = flagStr.Split(':');
                    name = split[0];
                    var v = split[1].ToLower();
                    val = v == "on" || v == "set" || v == "true" || v == "yes";
                }
                node.FlagSetters.Add(Tuple.Create(name, val));
            }
        }