void BuildTree(MapFile map, IEnumerable <JmfGroup> groups, IReadOnlyCollection <JmfEntity> entities)
        {
            Dictionary <int, int>       groupIds = new Dictionary <int, int>();       // file group id -> actual id
            Dictionary <int, MapObject> objTree  = new Dictionary <int, MapObject>(); // object id -> object

            int currentId = 2;                                                        // worldspawn is 1

            objTree[1] = map.Worldspawn;

            JmfEntity worldspawnEntity = entities.FirstOrDefault(x => x.Entity.ClassName == "worldspawn");

            if (worldspawnEntity != null)
            {
                map.Worldspawn.Properties = worldspawnEntity.Entity.Properties;
                map.Worldspawn.Color      = worldspawnEntity.Entity.Color;
                map.Worldspawn.SpawnFlags = worldspawnEntity.Entity.SpawnFlags;
                map.Worldspawn.Visgroups  = worldspawnEntity.Entity.Visgroups;
            }

            // Jackhammer doesn't allow a group within an entity, so groups
            // will only be children of worldspawn or another group. We can
            // build the group hierarchy immediately.
            List <JmfGroup> groupList  = groups.ToList();
            int             groupCount = groupList.Count;

            while (groupList.Any())
            {
                List <JmfGroup> pcs = groupList.Where(x => x.ID == x.ParentID || x.ParentID == 0 || groupIds.ContainsKey(x.ParentID)).ToList();
                foreach (JmfGroup g in pcs)
                {
                    int gid = currentId++;
                    groupIds[g.ID] = gid;
                    groupList.Remove(g);

                    Group group = new Group
                    {
                        Color = g.Color
                    };

                    int parentObjId = g.ID == g.ParentID || g.ParentID == 0 ? 1 : groupIds[g.ParentID];
                    objTree[parentObjId].Children.Add(group);
                    objTree[gid] = group;
                }

                if (groupList.Count == groupCount)
                {
                    break;                                // no groups processed, can't continue
                }
                groupCount = groupList.Count;
            }

            // For non-worldspawn solids, they are direct children of their entity.
            // For non-worldspawn entities, they're either a child of a group or of the worldspawn.
            foreach (JmfEntity entity in entities.Where(x => x != worldspawnEntity))
            {
                int parentId = groupIds.ContainsKey(entity.GroupID) ? groupIds[entity.GroupID] : 1;
                objTree[parentId].Children.Add(entity.Entity);

                // Put all the entity's solids straight underneath this entity
                entity.Entity.Children.AddRange(entity.Solids.Select(x => x.Solid));
            }

            // For worldspawn solids, they're either a child of a group or of the worldspawn.
            if (worldspawnEntity != null)
            {
                foreach (JmfSolid solid in worldspawnEntity.Solids)
                {
                    int parentId = groupIds.ContainsKey(solid.GroupID) ? groupIds[solid.GroupID] : 1;
                    objTree[parentId].Children.Add(solid.Solid);
                }
            }
        }
        List <JmfEntity> ReadEntities(MapFile map, BinaryReader br)
        {
            List <JmfEntity> entities = new List <JmfEntity>();

            while (br.BaseStream.Position < br.BaseStream.Length)
            {
                JmfEntity ent = new JmfEntity
                {
                    Entity = new Entity
                    {
                        ClassName = ReadString(br)
                    }
                };

                Vector3 origin = br.ReadVector3();
                ent.Entity.Properties["origin"] = $"{origin.X} {origin.Y} {origin.Z}";

                ent.Flags   = br.ReadInt32();
                ent.GroupID = br.ReadInt32();
                br.ReadInt32(); // group id again
                ent.Entity.Color = br.ReadRGBAColour();

                // useless (?) list of 13 strings
                for (int i = 0; i < 13; i++)
                {
                    ReadString(br);
                }

                ent.Entity.SpawnFlags = br.ReadInt32();

                br.ReadBytes(76); // unknown (!)

                int numProps = br.ReadInt32();
                for (int i = 0; i < numProps; i++)
                {
                    string key   = ReadString(br);
                    string value = ReadString(br);
                    if (key != null && value != null)
                    {
                        ent.Entity.Properties[key] = value;
                    }
                }

                ent.Entity.Visgroups = new List <int>();

                int numVisgroups = br.ReadInt32();
                for (int i = 0; i < numVisgroups; i++)
                {
                    ent.Entity.Visgroups.Add(br.ReadInt32());
                }

                int numSolids = br.ReadInt32();
                for (int i = 0; i < numSolids; i++)
                {
                    ent.Solids.Add(ReadSolid(map, br));
                }

                entities.Add(ent);
            }

            return(entities);
        }