Exemple #1
0
 public MapMobSpawn(int id, CoordS coord, int npcCount, List <int> npcList, int spawnRadius, SpawnMetadata spawnMetadata = null)
 {
     Id          = id;
     Coord       = coord;
     NpcCount    = npcCount;
     NpcList     = npcList;
     SpawnRadius = spawnRadius;
     SpawnData   = spawnMetadata;
 }
Exemple #2
0
 public MobSpawn(int id, CoordF pos, int spawnRadius, int maxPopulation, SpawnMetadata spawnData /*, List<int> mobIDs = null*/)
 {
     Id            = id;
     SpawnPosition = pos;
     SpawnRadius   = spawnRadius;
     MaxPopulation = maxPopulation;
     SpawnMobs     = SelectMobs(spawnData.Difficulty, spawnData.MinDifficulty, spawnData.Tags);
     SpawnData     = spawnData;
     Mobs          = new List <IFieldObject <Mob> >();
 }
Exemple #3
0
    private static void AddMobSpawn(SpawnMetadata mobSpawnDataBox, MapEntityMetadata metadata, IMS2RegionBoxSpawn boxSpawn)
    {
        int mobNpcCountBox    = mobSpawnDataBox?.Population ?? 6;
        int mobSpawnRadiusBox = 150;
        // TODO: This previously relied in "NpcList" to be set. NpcList is impossible to be set on
        // MS2RegionSpawn, it's only set for SpawnPointNPC.
        List <int> mobNpcListBox = new()
        {
            21000025 // Placeholder
        };

        metadata.MobSpawns.Add(new(boxSpawn.SpawnPointID, CoordS.FromVector3(boxSpawn.Position),
                                   mobNpcCountBox, mobNpcListBox, mobSpawnRadiusBox, mobSpawnDataBox));
        // "QR_10000264_" is Quest Reward Chest? This is tied to a MS2TriggerAgent making this object appear.
    }
Exemple #4
0
        static bool HandleListRespawnsCommand(CommandHandler handler, StringArguments args)
        {
            Player player = handler.GetSession().GetPlayer();
            Map    map    = player.GetMap();

            uint range = 0;

            if (!args.Empty())
            {
                range = args.NextUInt32();
            }

            Locale locale        = handler.GetSession().GetSessionDbcLocale();
            string stringOverdue = Global.ObjectMgr.GetCypherString(CypherStrings.ListRespawnsOverdue, locale);

            uint   zoneId   = player.GetZoneId();
            string zoneName = GetZoneName(zoneId, locale);

            for (SpawnObjectType type = 0; type < SpawnObjectType.NumSpawnTypes; type++)
            {
                if (range != 0)
                {
                    handler.SendSysMessage(CypherStrings.ListRespawnsRange, type, range);
                }
                else
                {
                    handler.SendSysMessage(CypherStrings.ListRespawnsZone, type, zoneName, zoneId);
                }

                handler.SendSysMessage(CypherStrings.ListRespawnsListheader);
                List <RespawnInfo> respawns = new();
                map.GetRespawnInfo(respawns, (SpawnObjectTypeMask)(1 << (int)type));
                foreach (RespawnInfo ri in respawns)
                {
                    SpawnMetadata data = Global.ObjectMgr.GetSpawnMetadata(ri.type, ri.spawnId);
                    if (data == null)
                    {
                        continue;
                    }

                    uint      respawnZoneId = 0;
                    SpawnData edata         = data.ToSpawnData();
                    if (edata != null)
                    {
                        respawnZoneId = map.GetZoneId(PhasingHandler.EmptyPhaseShift, edata.SpawnPoint);
                        if (range != 0)
                        {
                            if (!player.IsInDist(edata.SpawnPoint, range))
                            {
                                continue;
                            }
                        }
                        else
                        {
                            if (zoneId != respawnZoneId)
                            {
                                continue;
                            }
                        }
                    }

                    uint gridY = ri.gridId / MapConst.MaxGrids;
                    uint gridX = ri.gridId % MapConst.MaxGrids;

                    string respawnTime = ri.respawnTime > GameTime.GetGameTime() ? Time.secsToTimeString((ulong)(ri.respawnTime - GameTime.GetGameTime()), TimeFormat.ShortText) : stringOverdue;
                    handler.SendSysMessage($"{ri.spawnId} | {ri.entry} | [{gridX:2},{gridY:2}] | {GetZoneName(respawnZoneId, locale)} ({respawnZoneId}) | {respawnTime}{(map.IsSpawnGroupActive(data.spawnGroupData.groupId) ? "" : " (inactive)")}");
                }
            }
            return(true);
        }
Exemple #5
0
    private void BuildMetadata(string xblock, IEnumerable <IMapEntity> mapEntities)
    {
        if (xblock.EndsWith("_cn") || xblock.EndsWith("_jp") || xblock.EndsWith("_kr"))
        {
            return;
        }

        Match isParsableField = Regex.Match(xblock, @"^(\d{8})");

        if (!isParsableField.Success)
        {
            // TODO: Handle these later, if we need them. They're xblock files with some other names like
            //  character_test.xblock, login.xblock,
            return;
        }

        string mapId = isParsableField.Groups[1].Value;

        if (Maps.ContainsKey(mapId))
        {
            return;
        }

        Maps.Add(mapId, xblock); // Only used to check if we've visited this node before.

        // TODO: metadata should be keyed on xblock name, not mapId
        MapEntityMetadata metadata = new(int.Parse(mapId));

        foreach (IMapEntity entity in mapEntities)
        {
            switch (entity)
            {
            case IMS2Bounding bounding:
                if (bounding.EntityName.EndsWith("0") && metadata.BoundingBox0.Equals(CoordS.From(0, 0, 0)))
                {
                    metadata.BoundingBox0 = CoordS.FromVector3(bounding.Position);
                }
                else if (bounding.EntityName.EndsWith("1") && metadata.BoundingBox1.Equals(CoordS.From(0, 0, 0)))
                {
                    metadata.BoundingBox1 = CoordS.FromVector3(bounding.Position);
                }

                break;

            case IMS2PatrolData patrolData:
                string        patrolDataName = patrolData.EntityName.Replace("-", string.Empty);
                List <string> wayPointIds    = new();
                foreach (KeyValuePair <string, string> entry in patrolData.WayPoints)
                {
                    string wayPointId = entry.Value.Replace("-", string.Empty);
                    wayPointIds.Add(wayPointId);
                }

                List <string> arriveAnimations = new();
                foreach (KeyValuePair <string, string> entry in patrolData.ArriveAnims)
                {
                    arriveAnimations.Add(entry.Value);
                }

                List <string> approachAnimations = new();
                foreach (KeyValuePair <string, string> entry in patrolData.ApproachAnims)
                {
                    approachAnimations.Add(entry.Value);
                }

                List <int> arriveAnimationTimes = new();
                foreach (KeyValuePair <string, uint> entry in patrolData.ArriveAnimsTime)
                {
                    arriveAnimationTimes.Add((int)entry.Value);
                }

                metadata.PatrolDatas.Add(new(patrolDataName, wayPointIds, (int)patrolData.PatrolSpeed, patrolData.IsLoop, patrolData.IsAirWayPoint,
                                             arriveAnimations, approachAnimations, arriveAnimationTimes));
                break;

            case IMS2WayPoint wayPoint:
                metadata.WayPoints.Add(new(wayPoint.EntityId, wayPoint.IsVisible, CoordS.FromVector3(wayPoint.Position),
                                           CoordS.FromVector3(wayPoint.Rotation)));
                break;

            // TODO: This can probably be more generally handled as IMS2RegionSkill
            case IMS2HealingRegionSkillSound healingRegion:
                if (healingRegion.Position == default)
                {
                    continue;
                }

                metadata.HealingSpot.Add(CoordS.FromVector3(healingRegion.Position));
                break;

            case IMS2InteractObject interact:
                InteractObjectType type = GetInteractObjectType(interact.interactID);
                if (type == InteractObjectType.None)
                {
                    continue;
                }

                switch (interact)
                {
                case IMS2SimpleUiObject uiObject:
                    metadata.InteractObjects.Add(new(uiObject.EntityId, uiObject.interactID, uiObject.Enabled, type));
                    break;

                case IMS2InteractMesh interactMesh:
                    metadata.InteractObjects.Add(new(interactMesh.EntityId, interactMesh.interactID, interactMesh.IsVisible, type));
                    break;

                case IMS2Telescope telescope:
                    metadata.InteractObjects.Add(new(telescope.EntityId, telescope.interactID, telescope.Enabled, type));
                    break;

                case IMS2InteractActor interactActor:
                    metadata.InteractObjects.Add(new(interactActor.EntityId, interactActor.interactID, interactActor.IsVisible, type));
                    break;

                case IMS2InteractDisplay interactDisplay:
                    metadata.InteractObjects.Add(new(interactDisplay.EntityId, interactDisplay.interactID, interactDisplay.IsVisible, type));
                    break;
                }

                break;

            case ISpawnPoint spawn:
                switch (spawn)
                {
                // TODO: Parse "value" from NPCList.
                case IEventSpawnPointNPC eventSpawnNpc:         // trigger mob/npc spawns
                    List <string> npcIds = new();
                    npcIds.AddRange(eventSpawnNpc.NpcList.Keys);

                    metadata.EventNpcSpawnPoints.Add(new(eventSpawnNpc.SpawnPointID, eventSpawnNpc.NpcCount, npcIds, eventSpawnNpc.SpawnAnimation,
                                                         eventSpawnNpc.SpawnRadius,
                                                         CoordF.FromVector3(eventSpawnNpc.Position), CoordF.FromVector3(eventSpawnNpc.Rotation)));
                    break;

                case ISpawnPointPC pcSpawn:
                    metadata.PlayerSpawns.Add(
                        new(CoordS.FromVector3(pcSpawn.Position), CoordS.FromVector3(pcSpawn.Rotation)));
                    break;

                case ISpawnPointNPC npcSpawn:
                    // These tend to be vendors, shops, etc.
                    // If the name tag begins with SpawnPointNPC, I think these are mob spawn locations. Skipping these.
                    string npcIdStr = npcSpawn.NpcList.FirstOrDefault().Key ?? "0";
                    if (npcIdStr == "0" || !int.TryParse(npcIdStr, out int npcId))
                    {
                        continue;
                    }

                    MapNpc npc = new(npcId, npcSpawn.ModelName, npcSpawn.EntityName, CoordS.FromVector3(npcSpawn.Position),
                                     CoordS.FromVector3(npcSpawn.Rotation), npcSpawn.IsSpawnOnFieldCreate, npcSpawn.dayDie, npcSpawn.nightDie);
                    // Parse some additional flat supplemented data about this NPC.

                    metadata.Npcs.Add(npc);
                    break;
                }

                break;

            case IMS2RegionSpawnBase spawnBase:
                switch (spawnBase)
                {
                case IMS2RegionBoxSpawn boxSpawn:
                    SpawnMetadata mobSpawnDataBox = SpawnTagMap.ContainsKey(mapId) && SpawnTagMap[mapId].ContainsKey(boxSpawn.SpawnPointID)
                                ? SpawnTagMap[mapId][boxSpawn.SpawnPointID]
                                : null;
                    if (boxSpawn.ModelName.Contains("chest", StringComparison.CurrentCultureIgnoreCase))
                    {
                        metadata.MapChests.Add(new()
                        {
                            IsGolden = boxSpawn.ModelName.Contains("rare", StringComparison.CurrentCultureIgnoreCase),
                            Position = CoordF.FromVector3(boxSpawn.Position),
                            Rotation = CoordF.FromVector3(boxSpawn.Rotation),
                        });
                        continue;
                    }

                    AddMobSpawn(mobSpawnDataBox, metadata, boxSpawn);
                    break;

                case IMS2RegionSpawn regionSpawn:
                    SpawnMetadata mobSpawnData = SpawnTagMap.ContainsKey(mapId) && SpawnTagMap[mapId].ContainsKey(regionSpawn.SpawnPointID)
                                    ? SpawnTagMap[mapId][regionSpawn.SpawnPointID]
                                    : null;

                    int mobNpcCount = mobSpawnData?.Population ?? 6;

                    // TODO: This previously relied in "NpcList" to be set. NpcList is impossible to be set on
                    // MS2RegionSpawn, it's only set for SpawnPointNPC.
                    List <int> mobNpcList = new()
                    {
                        21000025         // Placeholder
                    };
                    metadata.MobSpawns.Add(new(regionSpawn.SpawnPointID, CoordS.FromVector3(regionSpawn.Position),
                                               mobNpcCount, mobNpcList, (int)regionSpawn.SpawnRadius, mobSpawnData));
                    break;
                }

                break;

            case IPortal portal:
                metadata.Portals.Add(new(portal.PortalID, portal.ModelName, portal.PortalEnable, portal.IsVisible, portal.MinimapIconVisible,
                                         portal.TargetFieldSN,
                                         CoordS.FromVector3(portal.Position), CoordS.FromVector3(portal.Rotation), portal.TargetPortalID, (PortalTypes)portal.PortalType));
                break;

            case IMS2Breakable breakable:
                switch (breakable)
                {
                case IMS2BreakableActor actor:
                    metadata.BreakableActors.Add(new(actor.EntityId, actor.Enabled, actor.hideTimer, actor.resetTimer));
                    break;

                case IMS2BreakableNIF nif:
                    metadata.BreakableNifs.Add(new(nif.EntityId, nif.Enabled, (int)nif.TriggerBreakableID, nif.hideTimer, nif.resetTimer));
                    break;
                }

                break;

            case IMS2TriggerObject triggerObject:
                switch (triggerObject)
                {
                case IMS2TriggerMesh triggerMesh:
                    metadata.TriggerMeshes.Add(new(triggerMesh.TriggerObjectID, triggerMesh.IsVisible));
                    break;

                case IMS2TriggerEffect triggerEffect:
                    metadata.TriggerEffects.Add(new(triggerEffect.TriggerObjectID, triggerEffect.IsVisible));
                    break;

                case IMS2TriggerCamera triggerCamera:
                    metadata.TriggerCameras.Add(new(triggerCamera.TriggerObjectID, triggerCamera.Enabled));
                    break;

                case IMS2TriggerBox triggerBox:
                    metadata.TriggerBoxes.Add(new(triggerBox.TriggerObjectID, CoordF.FromVector3(triggerBox.Position),
                                                  CoordF.FromVector3(triggerBox.ShapeDimensions)));
                    break;

                case IMS2TriggerLadder triggerLadder:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                    metadata.TriggerLadders.Add(new(triggerLadder.TriggerObjectID, triggerLadder.IsVisible));
                    break;

                case IMS2TriggerRope triggerRope:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                    metadata.TriggerRopes.Add(new(triggerRope.TriggerObjectID, triggerRope.IsVisible));
                    break;

                case IMS2TriggerPortal triggerPortal:
                    metadata.Portals.Add(new(triggerPortal.PortalID, triggerPortal.ModelName, triggerPortal.PortalEnable, triggerPortal.IsVisible,
                                             triggerPortal.MinimapIconVisible,
                                             triggerPortal.TargetFieldSN, CoordS.FromVector3(triggerPortal.Position), CoordS.FromVector3(triggerPortal.Rotation),
                                             triggerPortal.TargetPortalID, (PortalTypes)triggerPortal.PortalType, triggerPortal.TriggerObjectID));
                    break;

                case IMS2TriggerActor triggerActor:
                    metadata.TriggerActors.Add(new(triggerActor.TriggerObjectID, triggerActor.IsVisible, triggerActor.InitialSequence));
                    break;

                case IMS2TriggerCube triggerCube:
                    metadata.TriggerCubes.Add(new(triggerCube.TriggerObjectID, triggerCube.IsVisible));
                    break;

                case IMS2TriggerSound triggerSound:
                    metadata.TriggerSounds.Add(new(triggerSound.TriggerObjectID, triggerSound.Enabled));
                    break;

                case IMS2TriggerSkill triggerSkill:
                    metadata.TriggerSkills.Add(new(triggerSkill.TriggerObjectID, triggerSkill.skillID,
                                                   (short)triggerSkill.skillLevel, (byte)triggerSkill.count, CoordF.FromVector3(triggerSkill.Position)));
                    break;
                }

                break;

            case IMS2Liftable liftable:
                metadata.LiftableObjects.Add(new(liftable.EntityId, (int)liftable.ItemID, liftable.EffectQuestID, liftable.EffectQuestState,
                                                 liftable.ItemLifeTime, liftable.LiftableRegenCheckTime));
                break;

            case IMS2LiftableTargetBox liftableTargetBox:
                metadata.LiftableTargets.Add(new(liftableTargetBox.liftableTarget, CoordF.FromVector3(liftableTargetBox.Position),
                                                 CoordF.FromVector3(liftableTargetBox.ShapeDimensions)));
                break;

            case IMS2CubeProp prop:
                if (prop.IsObjectWeapon)
                {
                    List <int> weaponIds = prop.ObjectWeaponItemCode.Split(",").Select(int.Parse).ToList();
                    metadata.WeaponObjects.Add(new(CoordB.FromVector3(prop.Position), weaponIds));
                }

                break;

            case IMS2Vibrate vibrate:
                metadata.VibrateObjects.Add(new(vibrate.EntityId));
                break;
            }

            /* NPC Objects have a modelName of 8 digits followed by an underscore and a name that's the same thing,
             *  but with a number (for each instance on that map) after it
             *
             * IM_ Prefixed items are Interactable Meshes supplemented by data in "xml/table/interactobject.xml"
             * IA_ prefixed items are Interactable Actors (Doors, etc). Have an interactID, which is an event on interact.
             * "mixinMS2MapProperties" is generic field items
             *  "mixinMS2SalePost" - is for sale signs. Does a packet need to respond to this item?
             */
            /*
             *
             * // Unhandled Items:
             * // "mixinEventSpawnPointNPC",
             * // "mixinMS2Actor" as in "fa_fi_funct_irondoor_A01_"
             * // MS2RegionSkill as in "SkillObj_co_Crepper_C03_" (Only on 8xxxx and 9xxxxx maps)
             * // "mixinMS2FunctionCubeKFM" as in "ry_functobj_lamp_B01_", "ke_functobj_bath_B01_"
             * // "mixinMS2FunctionCubeNIF"
             * // "MS2MapProperties"->"MS2PhysXProp" that's not a weapon. Standard
             *
             * /*
             * if (Regex.Match(modelName, @"^\d{8}_").Success && Regex.Match(name, @"\d{1,3}").Success)
             * {
             *  // Parse non-permanent NPCs. These have no .flat files to supplement them.
             *  string npcListIndex = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild.Attributes["index"].Value ?? "-1";
             *  if (npcListIndex == "-1")
             *  {
             *      continue;
             *  }
             *  string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
             *  string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
             *  // metadata.Npcs.Add(new MapNpc(int.Parse(npcListIndex), modelName, name, ParseCoord(npcPositionValue), ParseCoord(npcRotationValue)));
             * }
             */
        }

        // No data on this map
        if (metadata.Npcs.Count == 0 && metadata.Portals.Count == 0 && metadata.PlayerSpawns.Count == 0)
        {
            return;
        }

        Entities.Add(metadata);
    }
        private void BuildMetadata(string xblock, IEnumerable <IMapEntity> mapEntities)
        {
            if (xblock.EndsWith("_cn") || xblock.EndsWith("_jp") || xblock.EndsWith("_kr"))
            {
                return;
            }

            Match isParsableField = Regex.Match(xblock, @"^(\d{8})");

            if (!isParsableField.Success)
            {
                // TODO: Handle these later, if we need them. They're xblock files with some other names like
                //  character_test.xblock, login.xblock,
                return;
            }

            string mapId = isParsableField.Groups[1].Value;

            if (Maps.ContainsKey(mapId))
            {
                return;
            }

            Maps.Add(mapId, xblock); // Only used to check if we've visited this node before.

            // TODO: metadata should be keyed on xblock name, not mapId
            MapEntityMetadata metadata = new MapEntityMetadata(int.Parse(mapId));

            foreach (IMapEntity entity in mapEntities)
            {
                switch (entity)
                {
                case IMS2Bounding bounding:
                    if (bounding.EntityName.EndsWith("0") && metadata.BoundingBox0.Equals(CoordS.From(0, 0, 0)))
                    {
                        metadata.BoundingBox0 = CoordS.FromVector3(bounding.Position);
                    }
                    else if (bounding.EntityName.EndsWith("1") && metadata.BoundingBox1.Equals(CoordS.From(0, 0, 0)))
                    {
                        metadata.BoundingBox1 = CoordS.FromVector3(bounding.Position);
                    }
                    break;

                case IMS2PatrolData patrolData:
                    string        patrolDataName = patrolData.EntityName.Replace("-", string.Empty);
                    List <string> wayPointIds    = new List <string>();
                    foreach (KeyValuePair <string, string> entry in patrolData.WayPoints)
                    {
                        string wayPointId = entry.Value.Replace("-", string.Empty);
                        wayPointIds.Add(wayPointId);
                    }

                    List <string> arriveAnimations = new List <string>();
                    foreach (KeyValuePair <string, string> entry in patrolData.ArriveAnims)
                    {
                        arriveAnimations.Add(entry.Value);
                    }

                    List <string> approachAnimations = new List <string>();
                    foreach (KeyValuePair <string, string> entry in patrolData.ApproachAnims)
                    {
                        approachAnimations.Add(entry.Value);
                    }

                    List <int> arriveAnimationTimes = new List <int>();
                    foreach (KeyValuePair <string, uint> entry in patrolData.ArriveAnimsTime)
                    {
                        arriveAnimationTimes.Add((int)entry.Value);
                    }
                    metadata.PatrolDatas.Add(new PatrolData(patrolDataName, wayPointIds, (int)patrolData.PatrolSpeed, patrolData.IsLoop, patrolData.IsAirWayPoint, arriveAnimations, approachAnimations, arriveAnimationTimes));
                    break;

                case IMS2WayPoint wayPoint:
                    metadata.WayPoints.Add(new WayPoint(wayPoint.EntityId, wayPoint.IsVisible, CoordS.FromVector3(wayPoint.Position), CoordS.FromVector3(wayPoint.Rotation)));
                    break;

                // TODO: This can probably be more generally handled as IMS2RegionSkill
                case IMS2HealingRegionSkillSound healingRegion:
                    if (healingRegion.Position == default)
                    {
                        continue;
                    }

                    metadata.HealingSpot.Add(CoordS.FromVector3(healingRegion.Position));
                    break;

                case IMS2InteractObject interact:     // TODO: this one is kinda f****d
                    if (interact.interactID == 0)
                    {
                        continue;
                    }
                    if (interact.ModelName.Contains("funct_extract_"))
                    {
                        metadata.InteractObjects.Add(new MapInteractObject(interact.EntityId, interact.EntityName,
                                                                           InteractObjectType.Extractor, interact.interactID));
                        continue;
                    }

                    if (entity is IMS2Telescope)
                    {
                        metadata.InteractObjects.Add(new MapInteractObject(interact.EntityId, interact.EntityName,
                                                                           InteractObjectType.Binoculars, interact.interactID));
                        continue;
                    }

                    if (interact.ModelName.EndsWith("hub") || interact.ModelName.EndsWith("vein"))
                    {
                        if (InteractRecipeMap.TryGetValue(interact.interactID, out int recipeId))
                        {
                            metadata.InteractObjects.Add(new MapInteractObject(interact.EntityId, interact.EntityName,
                                                                               InteractObjectType.Gathering, recipeId));
                        }
                        continue;
                    }

                    if (entity is IMS2InteractDisplay)
                    {
                        // TODO: Implement Interactive Displays like 02000183, Dark Wind Wanted Board (7bb334fe41f94182a9569ab884004c32)
                        // "mixinMS2InteractDisplay" ("ID_19100003_")
                    }

                    if (entity is IMS2InteractMesh)
                    {
                        // TODO: InteractMesh IS InteractObject, maybe shouldn't separate them...
                        metadata.InteractMeshes.Add(new MapInteractMesh(entity.EntityId, entity.EntityName));
                    }

                    metadata.InteractObjects.Add(new MapInteractObject(interact.EntityId, interact.EntityName,
                                                                       InteractObjectType.Unknown, interact.interactID));
                    break;

                case ISpawnPoint spawn:
                    switch (spawn)
                    {
                    // TODO: Parse "value" from NPCList.
                    case IEventSpawnPointNPC eventSpawnNpc:         // trigger mob/npc spawns
                        List <string> npcIds = new List <string>();
                        npcIds.AddRange(eventSpawnNpc.NpcList.Keys);

                        metadata.EventNpcSpawnPoints.Add(new MapEventNpcSpawnPoint(eventSpawnNpc.SpawnPointID, eventSpawnNpc.NpcCount, npcIds, eventSpawnNpc.SpawnAnimation, eventSpawnNpc.SpawnRadius,
                                                                                   CoordF.FromVector3(eventSpawnNpc.Position), CoordF.FromVector3(eventSpawnNpc.Rotation)));
                        break;

                    case ISpawnPointPC pcSpawn:
                        metadata.PlayerSpawns.Add(
                            new MapPlayerSpawn(CoordS.FromVector3(pcSpawn.Position), CoordS.FromVector3(pcSpawn.Rotation)));
                        break;

                    case ISpawnPointNPC npcSpawn:
                        // These tend to be vendors, shops, etc.
                        // If the name tag begins with SpawnPointNPC, I think these are mob spawn locations. Skipping these.
                        string npcIdStr = npcSpawn.NpcList.FirstOrDefault().Key ?? "0";
                        if (npcIdStr == "0" || !int.TryParse(npcIdStr, out int npcId))
                        {
                            continue;
                        }

                        MapNpc npc = new MapNpc(npcId, npcSpawn.ModelName, npcSpawn.EntityName, CoordS.FromVector3(npcSpawn.Position),
                                                CoordS.FromVector3(npcSpawn.Rotation), npcSpawn.IsSpawnOnFieldCreate, npcSpawn.dayDie, npcSpawn.nightDie);
                        // Parse some additional flat supplemented data about this NPC.

                        metadata.Npcs.Add(npc);
                        break;
                    }
                    break;

                case IMS2RegionSpawnBase spawnBase:
                    switch (spawnBase)
                    {
                    case IMS2RegionBoxSpawn boxSpawn:
                        SpawnMetadata mobSpawnDataBox =
                            (SpawnTagMap.ContainsKey(mapId) && SpawnTagMap[mapId].ContainsKey(boxSpawn.SpawnPointID))
                                        ? SpawnTagMap[mapId][boxSpawn.SpawnPointID]
                                        : null;

                        int mobNpcCountBox    = mobSpawnDataBox?.Population ?? 6;
                        int mobSpawnRadiusBox = 150;
                        // TODO: This previously relied in "NpcList" to be set. NpcList is impossible to be set on
                        // MS2RegionSpawn, it's only set for SpawnPointNPC.
                        List <int> mobNpcListBox = new List <int>
                        {
                            21000025         // Placeholder
                        };
                        metadata.MobSpawns.Add(new MapMobSpawn(boxSpawn.SpawnPointID, CoordS.FromVector3(boxSpawn.Position),
                                                               mobNpcCountBox, mobNpcListBox, mobSpawnRadiusBox, mobSpawnDataBox));
                        // "QR_10000264_" is Quest Reward Chest? This is tied to a MS2TriggerAgent making this object appear.
                        break;

                    case IMS2RegionSpawn regionSpawn:
                        SpawnMetadata mobSpawnData =
                            (SpawnTagMap.ContainsKey(mapId) && SpawnTagMap[mapId].ContainsKey(regionSpawn.SpawnPointID))
                                        ? SpawnTagMap[mapId][regionSpawn.SpawnPointID]
                                        : null;

                        int mobNpcCount = mobSpawnData?.Population ?? 6;

                        // TODO: This previously relied in "NpcList" to be set. NpcList is impossible to be set on
                        // MS2RegionSpawn, it's only set for SpawnPointNPC.
                        List <int> mobNpcList = new List <int>
                        {
                            21000025         // Placeholder
                        };
                        metadata.MobSpawns.Add(new MapMobSpawn(regionSpawn.SpawnPointID, CoordS.FromVector3(regionSpawn.Position),
                                                               mobNpcCount, mobNpcList, (int)regionSpawn.SpawnRadius, mobSpawnData));
                        break;
                    }
                    break;

                case IPortal portal:
                    metadata.Portals.Add(new MapPortal(portal.PortalID, portal.ModelName, portal.PortalEnable, portal.IsVisible, portal.MinimapIconVisible, portal.TargetFieldSN,
                                                       CoordS.FromVector3(portal.Position), CoordS.FromVector3(portal.Rotation), portal.TargetPortalID, (byte)portal.PortalType));
                    break;

                case IMS2Breakable breakable:
                    switch (breakable)
                    {
                    case IMS2BreakableActor actor:
                        metadata.BreakableActors.Add(new MapBreakableActorObject(actor.EntityId, actor.Enabled, actor.hideTimer, actor.resetTimer));
                        break;

                    case IMS2BreakableNIF nif:
                        metadata.BreakableNifs.Add(new MapBreakableNifObject(nif.EntityId, nif.Enabled, (int)nif.TriggerBreakableID, nif.hideTimer, nif.resetTimer));
                        break;
                    }
                    break;

                case IMS2TriggerObject triggerObject:
                    switch (triggerObject)
                    {
                    case IMS2TriggerMesh triggerMesh:
                        metadata.TriggerMeshes.Add(new MapTriggerMesh(triggerMesh.TriggerObjectID, triggerMesh.IsVisible));
                        break;

                    case IMS2TriggerEffect triggerEffect:
                        metadata.TriggerEffects.Add(new MapTriggerEffect(triggerEffect.TriggerObjectID, triggerEffect.IsVisible));
                        break;

                    case IMS2TriggerCamera triggerCamera:
                        metadata.TriggerCameras.Add(new MapTriggerCamera(triggerCamera.TriggerObjectID, triggerCamera.Enabled));
                        break;

                    case IMS2TriggerBox triggerBox:
                        metadata.TriggerBoxes.Add(new MapTriggerBox(triggerBox.TriggerObjectID, CoordF.FromVector3(triggerBox.Position), CoordF.FromVector3(triggerBox.ShapeDimensions)));
                        break;

                    case IMS2TriggerLadder triggerLadder:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                        metadata.TriggerLadders.Add(new MapTriggerLadder(triggerLadder.TriggerObjectID, triggerLadder.IsVisible));
                        break;

                    case IMS2TriggerRope triggerRope:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                        metadata.TriggerRopes.Add(new MapTriggerRope(triggerRope.TriggerObjectID, triggerRope.IsVisible));
                        break;

                    case IMS2TriggerPortal triggerPortal:
                        metadata.Portals.Add(new MapPortal(triggerPortal.PortalID, triggerPortal.ModelName, triggerPortal.PortalEnable, triggerPortal.IsVisible, triggerPortal.MinimapIconVisible,
                                                           triggerPortal.TargetFieldSN, CoordS.FromVector3(triggerPortal.Position), CoordS.FromVector3(triggerPortal.Rotation), triggerPortal.TargetPortalID, (byte)triggerPortal.PortalType, triggerPortal.TriggerObjectID));
                        break;

                    case IMS2TriggerActor triggerActor:
                        metadata.TriggerActors.Add(new MapTriggerActor(triggerActor.TriggerObjectID, triggerActor.IsVisible, triggerActor.InitialSequence));
                        break;

                    case IMS2TriggerCube triggerCube:
                        metadata.TriggerCubes.Add(new MapTriggerCube(triggerCube.TriggerObjectID, triggerCube.IsVisible));
                        break;

                    case IMS2TriggerSound triggerSound:
                        metadata.TriggerSounds.Add(new MapTriggerSound(triggerSound.TriggerObjectID, triggerSound.Enabled));
                        break;
                    }
                    break;

                case IMS2Vibrate vibrate:
                    metadata.VibrateObjects.Add(new MapVibrateObject(vibrate.EntityId));
                    break;

                case IPlaceable placeable:     // TODO: placeable might be too generic
                    // These are objects which you can place in the world
                    string nameCoord  = placeable.EntityName.ToLower();
                    Match  coordMatch = Regex.Match(nameCoord, @"-?\d+, -?\d+, -?\d+");
                    if (!coordMatch.Success)
                    {
                        continue;
                    }

                    // Only MS2MapProperties has ObjectWeaponItemCode
                    if (entity is not IMS2MapProperties mapProperties)
                    {
                        continue;
                    }

                    try
                    //TODO: The parser will output errors here, which are non-critical, yet need resolving later.
                    {
                        CoordB coord = CoordB.Parse(coordMatch.Value, ", ");
                        metadata.Objects.Add(new MapObject(coord, int.Parse(mapProperties.ObjectWeaponItemCode)));
                    }
                    catch (FormatException)
                    {
                        // ignored
                        //byte[] bytes = System.Text.Encoding.UTF8.GetBytes(mapProperties.ObjectWeaponItemCode);
                        //Console.WriteLine($"String in bytes: {Convert.ToHexString(bytes)}");
                    }
                    catch (OverflowException)
                    {
                        //byte[] bytes = System.Text.Encoding.UTF8.GetBytes(mapProperties.ObjectWeaponItemCode);
                        //Console.WriteLine($"String in bytes: {Convert.ToHexString(bytes)}");
                    }
                    break;
                }

                /* NPC Objects have a modelName of 8 digits followed by an underscore and a name that's the same thing,
                 *  but with a number (for each instance on that map) after it
                 *
                 * IM_ Prefixed items are Interactable Meshes supplemented by data in "xml/table/interactobject.xml"
                 * IA_ prefixed items are Interactable Actors (Doors, etc). Have an interactID, which is an event on interact.
                 * "mixinMS2MapProperties" is generic field items
                 *  "mixinMS2SalePost" - is for sale signs. Does a packet need to respond to this item?
                 */
                /*
                 *
                 * // Unhandled Items:
                 * // "mixinEventSpawnPointNPC",
                 * // "mixinMS2Actor" as in "fa_fi_funct_irondoor_A01_"
                 * // MS2RegionSkill as in "SkillObj_co_Crepper_C03_" (Only on 8xxxx and 9xxxxx maps)
                 * // "mixinMS2FunctionCubeKFM" as in "ry_functobj_lamp_B01_", "ke_functobj_bath_B01_"
                 * // "mixinMS2FunctionCubeNIF"
                 * // "MS2MapProperties"->"MS2PhysXProp" that's not a weapon. Standard
                 *
                 * /*
                 * if (Regex.Match(modelName, @"^\d{8}_").Success && Regex.Match(name, @"\d{1,3}").Success)
                 * {
                 *  // Parse non-permanent NPCs. These have no .flat files to supplement them.
                 *  string npcListIndex = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild.Attributes["index"].Value ?? "-1";
                 *  if (npcListIndex == "-1")
                 *  {
                 *      continue;
                 *  }
                 *  string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                 *  string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                 *  // metadata.Npcs.Add(new MapNpc(int.Parse(npcListIndex), modelName, name, ParseCoord(npcPositionValue), ParseCoord(npcRotationValue)));
                 * }
                 */
            }

            // No data on this map
            if (metadata.Npcs.Count == 0 && metadata.Portals.Count == 0 && metadata.PlayerSpawns.Count == 0)
            {
                return;
            }

            Entities.Add(metadata);
        }
        protected override List <MapEntityMetadata> Parse()
        {
            Entities = new List <MapEntityMetadata>();

            // fetch interactID and recipeID relation from xml (can be expanded to parse other xml info)
            InteractRecipeMap = new Dictionary <int, int>();
            foreach (PackFileEntry entry in Resources.XmlReader.Files
                     .Where(entry => Regex.Match(entry.Name, "table/interactobject_mastery").Success))
            {
                XmlDocument document      = Resources.XmlReader.GetXmlDocument(entry);
                XmlNodeList interactNodes = document.SelectNodes("/ms2/interact");

                foreach (XmlNode node in interactNodes)
                {
                    int     interactID    = int.Parse(node.Attributes["id"].Value);
                    XmlNode gatheringNode = node.SelectSingleNode("gathering");
                    int     recipeID      = int.Parse(gatheringNode.Attributes["receipeID"].Value);
                    InteractRecipeMap[interactID] = recipeID;
                }
            }

            // Get mob spawn ID and mob spawn information from xml (can be expanded to parse other xml info)
            SpawnTagMap = new Dictionary <string, Dictionary <int, SpawnMetadata> >();
            foreach (PackFileEntry entry in Resources.XmlReader.Files
                     .Where(entry => Regex.Match(entry.Name, "table/mapspawntag").Success))
            {
                XmlDocument document    = Resources.XmlReader.GetXmlDocument(entry);
                XmlNodeList regionNodes = document.SelectNodes("/ms2/region");

                foreach (XmlNode node in regionNodes)
                {
                    string mapID        = node.Attributes["mapCode"].Value;
                    int    spawnPointID = int.Parse(node.Attributes["spawnPointID"].Value);

                    int      difficulty    = int.Parse(node.Attributes["difficulty"].Value);
                    int      minDifficulty = int.Parse(node.Attributes["difficultyMin"].Value);
                    string[] spawnTags     = node.Attributes["tag"].Value.Split(",").Select(p => p.Trim()).ToArray();
                    if (!int.TryParse(node.Attributes["coolTime"].Value, out int spawnTime))
                    {
                        spawnTime = 0;
                    }

                    if (!int.TryParse(node.Attributes["population"].Value, out int population))
                    {
                        population = 0;
                    }

                    bool isPetSpawn = node.Attributes["petPopulation"] != null &&
                                      int.Parse(node.Attributes["petPopulation"].Value) > 0;

                    SpawnMetadata spawnData = new SpawnMetadata(spawnTags, population, spawnTime, difficulty,
                                                                minDifficulty, isPetSpawn);
                    if (!SpawnTagMap.ContainsKey(mapID))
                    {
                        SpawnTagMap[mapID] = new Dictionary <int, SpawnMetadata>();
                    }

                    SpawnTagMap[mapID][spawnPointID] = spawnData;
                }
            }

            // Iterate over map xblocks
            Maps = new Dictionary <string, string>();
            XBlockParser parser = new XBlockParser(Resources.ExportedReader, new FlatTypeIndex(Resources.ExportedReader));

            parser.Parse(BuildMetadata);

            // Since parsing is done in parallel, sort at the end for deterministic order.
            Entities.Sort((metadata1, metadata2) => metadata1.MapId.CompareTo(metadata2.MapId));
            return(Entities);
        }
        protected override List <MapEntityMetadata> Parse()
        {
            List <MapEntityMetadata> entities = new List <MapEntityMetadata>();

            /* Iterate over preset objects to later reference while iterating over exported maps
             * Key is modelName, value is parsed key/value from property
             * in json, this would look like:
             * {
             *   "11000003_": {"isSpawnPointNPC": "true", "SpawnPointID": "0", "SpawnRadius", "0"}
             * }
             */
            Dictionary <string, Dictionary <string, string> > mapObjects = new Dictionary <string, Dictionary <string, string> >();

            foreach (PackFileEntry entry in Resources.ExportedFiles
                     .Where(entry => Regex.Match(entry.Name, @"^flat/presets/presets (common|object|npc)/").Success)
                     .OrderBy(entry => entry.Name))
            {
                // Parse XML
                XmlDocument document  = Resources.ExportedMemFile.GetDocument(entry.FileHeader);
                string      modelName = document.DocumentElement.Attributes["name"].Value;

                // A local in-mem storage for all flat file supplementary data.
                Dictionary <string, string> thisNode = new Dictionary <string, string> {
                };

                foreach (XmlNode node in document.SelectNodes("model/mixin"))
                {
                    // These define the superclass this model inherits from. We store these as a property for later processing
                    // TODO: Should we only parse type="Active"?
                    if (node.Attributes["type"].Value.Equals("Active"))
                    {
                        string name = node.Attributes["name"].Value;
                        thisNode[$"mixin{name}"] = "true";  // isMS2InteractActor = "true";
                    }
                }

                foreach (XmlNode node in document.SelectNodes("model/property"))
                {
                    if (node.ChildNodes.Count > 0)  // hasChildren
                    {
                        thisNode[node.Attributes["name"].Value] = node.FirstChild?.Attributes["value"].Value;
                    }
                }
                mapObjects.Add(modelName, thisNode);
            }

            // fetch interactID and recipeID relation from xml (can be expanded to parse other xml info)
            Dictionary <int, int> interactRecipeMap = new Dictionary <int, int>();

            foreach (PackFileEntry entry in Resources.XmlFiles
                     .Where(entry => Regex.Match(entry.Name, "table/interactobject_mastery").Success))
            {
                XmlDocument document      = Resources.XmlMemFile.GetDocument(entry.FileHeader);
                XmlNodeList interactNodes = document.SelectNodes("/ms2/interact");

                foreach (XmlNode node in interactNodes)
                {
                    int     interactID    = int.Parse(node.Attributes["id"].Value);
                    XmlNode gatheringNode = node.SelectSingleNode("gathering");
                    int     recipeID      = int.Parse(gatheringNode.Attributes["receipeID"].Value);
                    interactRecipeMap[interactID] = recipeID;
                }
            }

            // Get mob spawn ID and mob spawn information from xml (can be expanded to parse other xml info)
            Dictionary <string, Dictionary <string, SpawnMetadata> > spawnTagMap = new Dictionary <string, Dictionary <string, SpawnMetadata> >();

            foreach (PackFileEntry entry in Resources.XmlFiles
                     .Where(entry => Regex.Match(entry.Name, "table/mapspawntag").Success))
            {
                XmlDocument document    = Resources.XmlMemFile.GetDocument(entry.FileHeader);
                XmlNodeList regionNodes = document.SelectNodes("/ms2/region");

                foreach (XmlNode node in regionNodes)
                {
                    string mapID        = node.Attributes["mapCode"].Value;
                    string spawnPointID = node.Attributes["spawnPointID"].Value;

                    int      difficulty    = int.Parse(node.Attributes["difficulty"].Value);
                    int      minDifficulty = int.Parse(node.Attributes["difficultyMin"].Value);
                    string[] spawnTags     = node.Attributes["tag"].Value.Split(",").Select(p => p.Trim()).ToArray();
                    if (!int.TryParse(node.Attributes["coolTime"].Value, out int spawnTime))
                    {
                        spawnTime = 0;
                    }
                    if (!int.TryParse(node.Attributes["population"].Value, out int population))
                    {
                        population = 0;
                    }
                    bool isPetSpawn = node.Attributes["petPopulation"] != null && int.Parse(node.Attributes["petPopulation"].Value) > 0;

                    SpawnMetadata spawnData = new SpawnMetadata(spawnTags, population, spawnTime, difficulty, minDifficulty, isPetSpawn);
                    if (!spawnTagMap.ContainsKey(mapID))
                    {
                        spawnTagMap[mapID] = new Dictionary <string, SpawnMetadata>();
                    }
                    spawnTagMap[mapID][spawnPointID] = spawnData;
                }
            }

            // Iterate over map xblocks
            Dictionary <string, string> maps = new Dictionary <string, string>();  // Have we already parsed this map?

            foreach (PackFileEntry entry in Resources.ExportedFiles
                     .Where(entry => entry.Name.StartsWith("xblock/"))
                     .OrderByDescending(entry => entry.Name))
            {
                Match isParsableField = Regex.Match(Path.GetFileNameWithoutExtension(entry.Name), @"^(\d{8})");
                if (!isParsableField.Success)
                {
                    // TODO: Handle these later, if we need them. They're xblock files with some other names like
                    //  character_test.xblock, login.xblock,
                    continue;
                }

                string mapId = isParsableField.Groups[1].Value;
                if (maps.ContainsKey(mapId))
                {
                    continue;
                }
                maps.Add(mapId, entry.Name);  // Only used to check if we've visited this node before.

                MapEntityMetadata metadata = new MapEntityMetadata(int.Parse(mapId));
                XmlDocument       document = Resources.ExportedMemFile.GetDocument(entry.FileHeader);

                foreach (XmlNode node in document.SelectNodes("/game/entitySet/entity"))
                {
                    string modelName = node.Attributes["modelName"].Value;  // Always maps to a .flat fileName for additional supplemented data
                    string name      = node.Attributes["name"].Value;

                    if (modelName == "MS2Bounding")
                    {
                        XmlNode blockCoord  = node.SelectSingleNode("property[@name='Position']");
                        CoordS  boundingBox = CoordS.Parse(blockCoord?.FirstChild.Attributes["value"].Value ?? "0, 0, 0");
                        if (name.EndsWith("0") && metadata.BoundingBox0.Equals(CoordS.From(0, 0, 0)))
                        {
                            metadata.BoundingBox0 = boundingBox;
                        }
                        else if (name.EndsWith("1") && metadata.BoundingBox1.Equals(CoordS.From(0, 0, 0)))
                        {
                            metadata.BoundingBox1 = boundingBox;
                        }
                    }
                    else if (modelName.StartsWith("Skill_HealingSpot"))
                    {
                        XmlNode blockCoord   = node.SelectSingleNode("property[@name='Position']");
                        CoordS  healingCoord = CoordS.Parse(blockCoord?.FirstChild.Attributes["value"].Value ?? "0, 0, 0");
                        if (healingCoord.Equals(CoordS.From(0, 0, 0)))
                        {
                            continue;
                        }
                        metadata.HealingSpot.Add(healingCoord);
                    }
                    else if (modelName == "MS2Telescope")
                    {
                        string uuid       = node.Attributes["id"].Value;
                        int    interactId = int.Parse(node.SelectSingleNode("property[@name='interactID']")?.FirstChild.Attributes["value"]?.Value);
                        metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Binoculars, interactId));
                    }
                    else if (modelName == "MS2InteractMesh")
                    {
                        if (node.SelectSingleNode("property[@name='interactID']") != null)
                        {
                            string uuid       = node.Attributes["id"].Value;
                            int    interactId = string.IsNullOrEmpty(node.SelectSingleNode("property[@name='interactID']").Value) ? 0 :
                                                int.Parse(node.SelectSingleNode("property[@name='interactID']")?.FirstChild.Attributes["value"]?.Value);
                            metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Extractor, interactId));
                        }
                    }
                    else if (modelName.Contains("hub") || modelName.Contains("vein"))
                    {
                        string uuid     = node.Attributes["id"].Value;
                        bool   parseRes = int.TryParse(node.SelectSingleNode("property[@name='interactID']")?.FirstChild.Attributes["value"]?.Value, out int interactId);
                        // use preset InteractID if unique interactID is invalid or does not exist
                        if (!parseRes || !interactRecipeMap.ContainsKey(interactId))
                        {
                            interactId = int.Parse(mapObjects[modelName]["interactID"]);
                        }
                        int recipeId = interactRecipeMap[interactId];
                        metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Gathering, interactId, recipeId));
                    }
                    else if (modelName == "SpawnPointPC")  // Player Spawn point on map
                    {
                        XmlNode playerCoord    = node.SelectSingleNode("property[@name='Position']");
                        XmlNode playerRotation = node.SelectSingleNode("property[@name='Rotation']");

                        string playerPositionValue = playerCoord?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        string playerRotationValue = playerRotation?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";

                        metadata.PlayerSpawns.Add(new MapPlayerSpawn(CoordS.Parse(playerPositionValue), CoordS.Parse(playerRotationValue)));
                    }
                    else if (modelName.StartsWith("MS2RegionSpawn"))  // Mob Spawn point on map
                    {
                        XmlNode spawnPointID = node.SelectSingleNode("property[@name='SpawnPointID']");
                        XmlNode mobCoord     = node.SelectSingleNode("property[@name='Position']");
                        XmlNode npcCount     = node.SelectSingleNode("property[@name='NpcCount']");
                        XmlNode npcList      = node.SelectSingleNode("property[@name='NpcList']");
                        XmlNode spawnRadius  = node.SelectSingleNode("property[@name='SpawnRadius']");

                        string        mobSpawnPointID  = spawnPointID?.FirstChild.Attributes["value"]?.Value ?? mapObjects[modelName]["SpawnPointID"];
                        string        mobPositionValue = mobCoord?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        SpawnMetadata mobSpawnData     = (spawnTagMap.ContainsKey(mapId) && spawnTagMap[mapId].ContainsKey(mobSpawnPointID)) ? spawnTagMap[mapId][mobSpawnPointID] : null; // Do we need this spawn data (?)
                        int           mobNpcCount      = mobSpawnData?.Population ?? 6;
                        List <int>    mobNpcList       = new List <int>();
                        if (npcList != null)
                        {
                            foreach (XmlNode npcNode in npcList.ChildNodes)
                            {
                                // TODO: add mob spawn count to each ID
                                mobNpcList.Add(int.Parse(npcNode.Attributes["index"].Value));
                            }
                        }
                        if (mobNpcList.Count == 0)
                        {
                            mobNpcList.Add(21000025);  // Placeholder
                        }
                        int mobSpawnRadius = int.Parse(spawnRadius?.FirstChild.Attributes["value"]?.Value ?? "150");

                        metadata.MobSpawns.Add(new MapMobSpawn(int.Parse(mobSpawnPointID), CoordS.Parse(mobPositionValue), mobNpcCount, mobNpcList, mobSpawnRadius, mobSpawnData));
                    }
                    else if (modelName == "Portal_entrance" || modelName == "Portal_cube" || modelName.Contains("Portal_memberance"))
                    {
                        XmlNode portalIdNode    = node.SelectSingleNode("property[@name='PortalID']");
                        XmlNode targetFieldNode = node.SelectSingleNode("property[@name='TargetFieldSN']");
                        XmlNode targetIdNode    = node.SelectSingleNode("property[@name='TargetPortalID']");
                        if (portalIdNode == null)
                        {
                            continue;
                        }

                        XmlNode visibleNode        = node.SelectSingleNode("property[@name='IsVisible']");
                        XmlNode enabledNode        = node.SelectSingleNode("property[@name='PortalEnable']");
                        XmlNode minimapVisibleNode = node.SelectSingleNode("property[@name='MinimapIconVisible']");
                        XmlNode coordNode          = node.SelectSingleNode("property[@name='Position']");
                        XmlNode rotationNode       = node.SelectSingleNode("property[@name='Rotation']");

                        if (!bool.TryParse(visibleNode?.FirstChild.Attributes["value"].Value, out bool visibleValue))
                        {
                            visibleValue = true;
                        }
                        if (!bool.TryParse(enabledNode?.FirstChild.Attributes["value"].Value, out bool enabledValue))
                        {
                            enabledValue = true;
                        }
                        if (!bool.TryParse(minimapVisibleNode?.FirstChild.Attributes["value"].Value, out bool minimapVisibleValue))
                        {
                            minimapVisibleValue = true;
                        }

                        int target = 0;
                        if (targetFieldNode != null)
                        {
                            target = int.Parse(targetFieldNode.FirstChild.Attributes["value"].Value ?? "0");
                        }
                        int portalId       = int.Parse(portalIdNode?.FirstChild.Attributes["value"].Value);
                        int targetPortalId = 0;
                        if (targetIdNode != null)
                        {
                            targetPortalId = int.Parse(targetIdNode?.FirstChild.Attributes["value"].Value);
                        }
                        string positionValue = coordNode?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        string rotationValue = rotationNode?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";

                        MapPortalFlag flags = visibleValue ? MapPortalFlag.Visible : MapPortalFlag.None;
                        flags |= enabledValue ? MapPortalFlag.Enabled : MapPortalFlag.None;
                        flags |= minimapVisibleValue ? MapPortalFlag.MinimapVisible : MapPortalFlag.None;

                        CoordS position = CoordS.Parse(positionValue);
                        CoordS rotation = CoordS.Parse(rotationValue);
                        metadata.Portals.Add(new MapPortal(portalId, modelName, flags, target, position, rotation, targetPortalId));
                    }
                    else if (modelName == "SpawnPointNPC" && !name.StartsWith("SpawnPointNPC"))
                    {
                        // These tend to be vendors, shops, etc.
                        // If the name tag begins with SpawnPointNPC, I think these are mob spawn locations. Skipping these.
                        string npcId = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild.Attributes["index"].Value ?? "0";
                        if (npcId == "0")
                        {
                            continue;
                        }
                        string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        MapNpc thisNpc          = new MapNpc(int.Parse(npcId), modelName, name, CoordS.Parse(npcPositionValue), CoordS.Parse(npcRotationValue));
                        thisNpc.IsSpawnOnFieldCreate = true;
                        metadata.Npcs.Add(thisNpc);
                    }
                    else if (modelName == "EventSpawnPointNPC")
                    {
                        string npcId = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild?.Attributes["index"].Value ?? "0";
                        if (npcId == "0")
                        {
                            continue;
                        }
                        string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                        MapNpc thisNpc          = new MapNpc(int.Parse(npcId), modelName, name, CoordS.Parse(npcPositionValue), CoordS.Parse(npcRotationValue));
                        metadata.Npcs.Add(thisNpc);
                    }
                    // Parse the rest of the objects in the xblock if they have a flat component.
                    else if (mapObjects.ContainsKey(modelName))  // There was .flat file data about this item
                    {
                        /* NPC Objects have a modelName of 8 digits followed by an underscore and a name that's the same thing,
                         *  but with a number (for each instance on that map) after it
                         *
                         * IM_ Prefixed items are Interactable Meshes supplemented by data in "xml/table/interactobject.xml"
                         * IA_ prefixed items are Interactable Actors (Doors, etc). Have an interactID, which is an event on interact.
                         * "mixinMS2MapProperties" is generic field items
                         *  "mixinMS2SalePost" - is for sale signs. Does a packet need to respond to this item?
                         */
                        Dictionary <string, string> modelData = mapObjects[modelName];

                        if (modelData.ContainsKey("mixinMS2TimeShowSetting") ||
                            modelData.ContainsKey("mixinMS2Sound") ||
                            modelData.ContainsKey("mixinMS2SalePost") ||
                            modelData.ContainsKey("mixinMS2Actor") ||  // "fa_fi_funct_irondoor_A01_"
                            modelData.ContainsKey("mixinMS2RegionSkill") ||
                            modelData.ContainsKey("mixinMS2FunctionCubeKFM") ||
                            modelData.ContainsKey("mixinMS2FunctionCubeNIF")
                            )
                        {
                            continue;  // Skip these for now.
                        }
                        else if (modelData.ContainsKey("mixinMS2InteractObject"))
                        {
                            if (modelData.ContainsKey("mixinMS2InteractMesh"))
                            {
                                // TODO: Implement mesh packet
                                string uuid = node.Attributes["id"].Value.ToLower();
                                metadata.InteractMeshes.Add(new MapInteractMesh(uuid, name));
                            }
                            else if (modelData.ContainsKey("mixinMS2InteractActor"))
                            {
                                string uuid = node.Attributes["id"].Value.ToLower();
                                metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Unknown, 0));
                            }
                            else if (modelData.ContainsKey("mixinMS2InteractDisplay"))
                            {
                                // TODO: Implement Interactive Displays like 02000183, Dark Wind Wanted Board (7bb334fe41f94182a9569ab884004c32)
                                // "mixinMS2InteractDisplay" ("ID_19100003_")
                            }
                            else
                            {
                                // TODO: Any others?

                                if (name.Contains("funct_extract_"))
                                {
                                    string uuid = node.Attributes["id"].Value.ToLower();
                                    metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Extractor, 0));
                                }
                                else if (name.Contains("funct_telescope_"))
                                {
                                    string uuid       = node.Attributes["id"].Value;
                                    int    interactId = int.Parse(node.SelectSingleNode("property[@name='interactID']")?.FirstChild.Attributes["value"]?.Value);
                                    metadata.InteractObjects.Add(new MapInteractObject(uuid, name, InteractObjectType.Binoculars, interactId));
                                }
                            }
                        }
                        else if (modelData.ContainsKey("mixinSpawnPointNPC"))
                        {
                            // These can be full natural spawns, or only spawnable as a reaction to a quest, or something else as well.
                            string npcId      = Regex.Match(modelName, @"^(\d{8})_").Groups[1].Value;
                            string indexNpcId = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild.Attributes["index"].Value ?? "0";
                            if (indexNpcId != "0")
                            {
                                npcId = indexNpcId;
                            }
                            string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                            string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                            MapNpc thisNpc          = new MapNpc(int.Parse(npcId), modelName, name, CoordS.Parse(npcPositionValue), CoordS.Parse(npcRotationValue));
                            // Parse some additional flat supplemented data about this NPC.
                            if (mapObjects.ContainsKey(modelName))
                            {
                                Dictionary <string, string> thisFlatData = mapObjects[modelName];

                                // Parse IsSpawnOnFieldCreate
                                thisNpc.IsSpawnOnFieldCreate = thisFlatData["IsSpawnOnFieldCreate"] == "True";
                                thisNpc.IsDayDie             = thisFlatData["dayDie"] == "True";
                                thisNpc.IsNightDie           = thisFlatData["nightDie"] == "True";
                            }
                            metadata.Npcs.Add(thisNpc);
                        }
                        else if (modelData.ContainsKey("mixinMS2BreakableActor"))
                        {
                            // TODO: Do we need to parse these as some special NPC object?
                        }
                        else if (modelData.ContainsKey("mixinMS2Placeable"))
                        {
                            // These are objects which you can place in the world
                            string nameCoord = node.Attributes["name"].Value.ToLower();

                            Match coordMatch = Regex.Match(nameCoord, @"[\-]?\d+[,]\s[\-]?\d+[,]\s[\-]?\d+");

                            if (!coordMatch.Success)
                            {
                                continue;
                            }

                            CoordB coord = CoordB.Parse(coordMatch.Value, ", ");
                            metadata.Objects.Add(new MapObject(coord, int.Parse(modelData["ObjectWeaponItemCode"])));
                        }
                        else if (modelData.ContainsKey("mixinMS2CubeProp"))
                        {
                            if (!modelData.ContainsKey("ObjectWeaponItemCode"))
                            {
                                continue;
                            }
                            string weaponId = modelData["ObjectWeaponItemCode"] ?? "0";
                            if (!weaponId.Equals("0"))
                            {
                                // Extract the coordinate from the name. rhy tried just grabbing position, but it wasn't reliable.
                                Match coordMatch = Regex.Match(name, @"[\-]?\d+[,]\s[\-]?\d+[,]\s[\-]?\d+");
                                if (coordMatch.Success)
                                {
                                    metadata.Objects.Add(new MapObject(CoordB.Parse(coordMatch.Value, ", "), int.Parse(weaponId)));
                                }
                            }
                        }
                        else if (modelData.ContainsKey("mixinMS2Breakable"))
                        {
                            // "mixinMS2Breakable"  But not "mixinMS2BreakableActor", as in ke_fi_prop_buoy_A01_ or el_move_woodbox_B04_
                        }
                        else if (modelData.ContainsKey("mixinMS2RegionBoxSpawn"))
                        {
                            // "QR_10000264_" is Quest Reward Chest? This is tied to a MS2TriggerAgent making this object appear.
                        }
                        // Unhandled Items:
                        // "mixinEventSpawnPointNPC",
                        // "mixinMS2Actor" as in "fa_fi_funct_irondoor_A01_"
                        // MS2RegionSkill as in "SkillObj_co_Crepper_C03_" (Only on 8xxxx and 9xxxxx maps)
                        // "mixinMS2FunctionCubeKFM" as in "ry_functobj_lamp_B01_", "ke_functobj_bath_B01_"
                        // "mixinMS2FunctionCubeNIF"
                        // "MS2MapProperties"->"MS2PhysXProp" that's not a weapon. Standard
                    }

                    /*
                     * if (Regex.Match(modelName, @"^\d{8}_").Success && Regex.Match(name, @"\d{1,3}").Success)
                     * {
                     *  // Parse non-permanent NPCs. These have no .flat files to supplement them.
                     *  string npcListIndex = node.SelectSingleNode("property[@name='NpcList']")?.FirstChild.Attributes["index"].Value ?? "-1";
                     *  if (npcListIndex == "-1")
                     *  {
                     *      continue;
                     *  }
                     *  string npcPositionValue = node.SelectSingleNode("property[@name='Position']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                     *  string npcRotationValue = node.SelectSingleNode("property[@name='Rotation']")?.FirstChild.Attributes["value"].Value ?? "0, 0, 0";
                     *  // metadata.Npcs.Add(new MapNpc(int.Parse(npcListIndex), modelName, name, ParseCoord(npcPositionValue), ParseCoord(npcRotationValue)));
                     * }
                     */
                }

                // No data on this map
                if (metadata.Npcs.Count == 0 && metadata.Portals.Count == 0 && metadata.PlayerSpawns.Count == 0)
                {
                    continue;
                }

                entities.Add(metadata);
            }
            Console.Out.WriteLine($"Parsed {entities.Count} entities");
            return(entities);
        }
        protected override List <MapEntityMetadata> Parse()
        {
            // Get InteractObject Types
            InteractTypes = new Dictionary <int, InteractObjectType>();
            foreach (PackFileEntry entry in Resources.XmlReader.Files)
            {
                if (!entry.Name.StartsWith("table/interactobject"))
                {
                    continue;
                }

                XmlDocument document      = Resources.XmlReader.GetXmlDocument(entry);
                XmlNodeList interactNodes = document.GetElementsByTagName("interact");
                foreach (XmlNode node in interactNodes)
                {
                    string locale = node.Attributes["locale"]?.Value ?? "";
                    if (locale != "NA" && locale != "")
                    {
                        continue;
                    }

                    int interactId = int.Parse(node.Attributes["id"].Value);
                    _ = Enum.TryParse(node.Attributes["type"].Value, out InteractObjectType objectType);

                    InteractTypes[interactId] = objectType;
                }
            }

            Entities = new List <MapEntityMetadata>();

            // Get mob spawn ID and mob spawn information from xml (can be expanded to parse other xml info)
            SpawnTagMap = new Dictionary <string, Dictionary <int, SpawnMetadata> >();
            foreach (PackFileEntry entry in Resources.XmlReader.Files.Where(x => x.Name.StartsWith("table/mapspawntag")))
            {
                XmlDocument document    = Resources.XmlReader.GetXmlDocument(entry);
                XmlNodeList regionNodes = document.SelectNodes("/ms2/region");

                foreach (XmlNode node in regionNodes)
                {
                    string mapID        = node.Attributes["mapCode"].Value;
                    int    spawnPointID = int.Parse(node.Attributes["spawnPointID"].Value);

                    int      difficulty    = int.Parse(node.Attributes["difficulty"].Value);
                    int      minDifficulty = int.Parse(node.Attributes["difficultyMin"].Value);
                    string[] spawnTags     = node.Attributes["tag"].Value.Split(",").Select(p => p.Trim()).ToArray();
                    if (!int.TryParse(node.Attributes["coolTime"].Value, out int spawnTime))
                    {
                        spawnTime = 0;
                    }

                    if (!int.TryParse(node.Attributes["population"].Value, out int population))
                    {
                        population = 0;
                    }

                    _ = int.TryParse(node.Attributes["petPopulation"]?.Value, out int petPopulation);
                    bool isPetSpawn = petPopulation > 0;

                    SpawnMetadata spawnData = new SpawnMetadata(spawnTags, population, spawnTime, difficulty, minDifficulty, isPetSpawn);
                    if (!SpawnTagMap.ContainsKey(mapID))
                    {
                        SpawnTagMap[mapID] = new Dictionary <int, SpawnMetadata>();
                    }

                    SpawnTagMap[mapID][spawnPointID] = spawnData;
                }
            }

            // Iterate over map xblocks
            Maps = new Dictionary <string, string>();
            XBlockParser parser = new XBlockParser(Resources.ExportedReader, new FlatTypeIndex(Resources.ExportedReader));

            parser.Parse(BuildMetadata);

            // Since parsing is done in parallel, sort at the end for deterministic order.
            Entities.Sort((metadata1, metadata2) => metadata1.MapId.CompareTo(metadata2.MapId));
            return(Entities);
        }
Exemple #10
0
    private void BuildMetadata(string xblock, IEnumerable <IMapEntity> mapEntities)
    {
        if (xblock.EndsWith("_cn") || xblock.EndsWith("_jp") || xblock.EndsWith("_kr"))
        {
            return;
        }

        MapMetadata mapMetadata = MapMetadatas.FirstOrDefault(x => x.XBlockName == xblock);

        if (mapMetadata is null)
        {
            return;
        }

        MapEntityMetadata mapEntity = mapMetadata.Entities;

        List <IMS2WayPoint>   tempWaypoints  = new();
        List <IMS2PatrolData> tempPatrolData = new();

        foreach (IMapEntity entity in mapEntities)
        {
            if (entity is IMS2CubeProp cube)
            {
                MapBlock mapBlock = new()
                {
                    Coord         = CoordS.FromVector3(cube.Position),
                    Type          = cube.CubeType,
                    SaleableGroup = cube.CubeSalableGroup,
                    Attribute     = cube.MapAttribute
                };

                mapMetadata.Blocks.TryAdd(mapBlock.Coord, mapBlock);
            }

            switch (entity)
            {
            case IMS2Bounding bounding:
                if (bounding.EntityName.EndsWith("0") && mapEntity.BoundingBox0.Equals(CoordS.From(0, 0, 0)))
                {
                    mapEntity.BoundingBox0 = CoordS.FromVector3(bounding.Position);
                }
                else if (bounding.EntityName.EndsWith("1") && mapEntity.BoundingBox1.Equals(CoordS.From(0, 0, 0)))
                {
                    mapEntity.BoundingBox1 = CoordS.FromVector3(bounding.Position);
                }

                break;

            case IMS2PatrolData patrolData:
                tempPatrolData.Add(patrolData);
                break;

            case IMS2WayPoint wayPoint:
                tempWaypoints.Add(wayPoint);
                break;

            // TODO: This can probably be more generally handled as IMS2RegionSkill
            case IMS2HealingRegionSkillSound healingRegion:
                if (healingRegion.Position == default)
                {
                    continue;
                }

                mapEntity.HealingSpot.Add(CoordS.FromVector3(healingRegion.Position));
                break;

            case IMS2InteractObject interact:
                InteractObjectType type = GetInteractObjectType(interact.interactID);
                if (type == InteractObjectType.None)
                {
                    continue;
                }

                switch (interact)
                {
                case IMS2SimpleUiObject uiObject:
                    mapEntity.InteractObjects.Add(new(uiObject.EntityId, uiObject.interactID, uiObject.Enabled, type,
                                                      CoordF.FromVector3(uiObject.Position),
                                                      CoordF.FromVector3(uiObject.Rotation)));
                    break;

                case IMS2InteractMesh interactMesh:
                    mapEntity.InteractObjects.Add(new(interactMesh.EntityId, interactMesh.interactID, interactMesh.IsVisible, type,
                                                      CoordF.FromVector3(interactMesh.Position),
                                                      CoordF.FromVector3(interactMesh.Rotation)));
                    break;

                case IMS2Telescope telescope:
                    mapEntity.InteractObjects.Add(new(telescope.EntityId, telescope.interactID, telescope.Enabled, type,
                                                      CoordF.FromVector3(telescope.Position),
                                                      CoordF.FromVector3(telescope.Rotation)));
                    break;

                case IMS2InteractActor interactActor:
                    mapEntity.InteractObjects.Add(new(interactActor.EntityId, interactActor.interactID, interactActor.IsVisible, type
                                                      , CoordF.FromVector3(interactActor.Position),
                                                      CoordF.FromVector3(interactActor.Rotation)));
                    break;

                case IMS2InteractDisplay interactDisplay:
                    mapEntity.InteractObjects.Add(new(interactDisplay.EntityId, interactDisplay.interactID, interactDisplay.IsVisible, type,
                                                      CoordF.FromVector3(interactDisplay.Position),
                                                      CoordF.FromVector3(interactDisplay.Rotation)));
                    break;
                }

                break;

            case ISpawnPoint spawn:
                switch (spawn)
                {
                // TODO: Parse "value" from NPCList.
                case IEventSpawnPointNPC eventSpawnNpc:         // trigger mob/npc spawns
                    List <string> npcIds = new();
                    npcIds.AddRange(eventSpawnNpc.NpcList.Keys);

                    mapEntity.EventNpcSpawnPoints.Add(new(eventSpawnNpc.SpawnPointID, eventSpawnNpc.NpcCount, npcIds, eventSpawnNpc.SpawnAnimation,
                                                          eventSpawnNpc.SpawnRadius,
                                                          CoordF.FromVector3(eventSpawnNpc.Position), CoordF.FromVector3(eventSpawnNpc.Rotation)));
                    break;

                case ISpawnPointPC pcSpawn:
                    mapEntity.PlayerSpawns.Add(new(CoordS.FromVector3(pcSpawn.Position), CoordS.FromVector3(pcSpawn.Rotation)));
                    break;

                case ISpawnPointNPC npcSpawn:
                    // These tend to be vendors, shops, etc.
                    // If the name tag begins with SpawnPointNPC, I think these are mob spawn locations. Skipping these.
                    string npcIdStr = npcSpawn.NpcList.FirstOrDefault().Key ?? "0";
                    if (npcIdStr == "0" || !int.TryParse(npcIdStr, out int npcId))
                    {
                        continue;
                    }

                    MapNpc npc = new(npcId, npcSpawn.ModelName, npcSpawn.EntityName, CoordS.FromVector3(npcSpawn.Position),
                                     CoordS.FromVector3(npcSpawn.Rotation), npcSpawn.IsSpawnOnFieldCreate, npcSpawn.dayDie, npcSpawn.nightDie, npcSpawn.PatrolData);
                    // Parse some additional flat supplemented data about this NPC.

                    mapEntity.Npcs.Add(npc);
                    break;
                }

                break;

            case IMS2RegionSpawnBase spawnBase:
                SpawnMetadata spawnData = SpawnTagMap.ContainsKey(mapMetadata.Id) && SpawnTagMap[mapMetadata.Id].ContainsKey(spawnBase.SpawnPointID)
                        ? SpawnTagMap[mapMetadata.Id][spawnBase.SpawnPointID]
                        : null;
                switch (spawnBase)
                {
                case IMS2RegionBoxSpawn boxSpawn:
                    if (boxSpawn.ModelName.Contains("chest", StringComparison.CurrentCultureIgnoreCase))
                    {
                        mapEntity.MapChests.Add(new()
                        {
                            IsGolden = boxSpawn.ModelName.Contains("rare", StringComparison.CurrentCultureIgnoreCase),
                            Position = CoordF.FromVector3(boxSpawn.Position),
                            Rotation = CoordF.FromVector3(boxSpawn.Rotation)
                        });
                        continue;
                    }

                    AddMobSpawn(spawnData, mapEntity, boxSpawn);
                    break;

                case IMS2RegionSpawn regionSpawn:
                    int mobNpcCount = spawnData?.Population ?? 6;

                    // TODO: This previously relied in "NpcList" to be set. NpcList is impossible to be set on
                    // MS2RegionSpawn, it's only set for SpawnPointNPC.
                    List <int> mobNpcList = new()
                    {
                        21000025         // Placeholder
                    };
                    mapEntity.MobSpawns.Add(new(regionSpawn.SpawnPointID, CoordS.FromVector3(regionSpawn.Position),
                                                mobNpcCount, mobNpcList, (int)regionSpawn.SpawnRadius, spawnData));
                    break;
                }

                break;

            case IPortal portal:
                mapEntity.Portals.Add(new(portal.PortalID, portal.ModelName, portal.PortalEnable, portal.IsVisible, portal.MinimapIconVisible,
                                          portal.TargetFieldSN,
                                          CoordS.FromVector3(portal.Position), CoordS.FromVector3(portal.Rotation), portal.TargetPortalID, (PortalTypes)portal.PortalType));
                break;

            case IMS2Breakable breakable:
                switch (breakable)
                {
                case IMS2BreakableActor actor:
                    mapEntity.BreakableActors.Add(new(actor.EntityId, actor.Enabled, actor.hideTimer, actor.resetTimer));
                    break;

                case IMS2BreakableNIF nif:
                    mapEntity.BreakableNifs.Add(new(nif.EntityId, nif.Enabled, (int)nif.TriggerBreakableID, nif.hideTimer, nif.resetTimer));
                    break;
                }

                break;

            case IMS2TriggerObject triggerObject:
                switch (triggerObject)
                {
                case IMS2TriggerMesh triggerMesh:
                    mapEntity.TriggerMeshes.Add(new(triggerMesh.TriggerObjectID, triggerMesh.IsVisible));
                    break;

                case IMS2TriggerEffect triggerEffect:
                    mapEntity.TriggerEffects.Add(new(triggerEffect.TriggerObjectID, triggerEffect.IsVisible));
                    break;

                case IMS2TriggerCamera triggerCamera:
                    mapEntity.TriggerCameras.Add(new(triggerCamera.TriggerObjectID, triggerCamera.Enabled));
                    break;

                case IMS2TriggerBox triggerBox:
                    mapEntity.TriggerBoxes.Add(new(triggerBox.TriggerObjectID, CoordF.FromVector3(triggerBox.Position),
                                                   CoordF.FromVector3(triggerBox.ShapeDimensions)));
                    break;

                case IMS2TriggerLadder triggerLadder:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                    mapEntity.TriggerLadders.Add(new(triggerLadder.TriggerObjectID, triggerLadder.IsVisible));
                    break;

                case IMS2TriggerRope triggerRope:         // TODO: Find which parameters correspond to animationeffect (bool) and animation delay (int?)
                    mapEntity.TriggerRopes.Add(new(triggerRope.TriggerObjectID, triggerRope.IsVisible));
                    break;

                case IMS2TriggerPortal triggerPortal:
                    mapEntity.Portals.Add(new(triggerPortal.PortalID, triggerPortal.ModelName, triggerPortal.PortalEnable, triggerPortal.IsVisible,
                                              triggerPortal.MinimapIconVisible,
                                              triggerPortal.TargetFieldSN, CoordS.FromVector3(triggerPortal.Position), CoordS.FromVector3(triggerPortal.Rotation),
                                              triggerPortal.TargetPortalID, (PortalTypes)triggerPortal.PortalType, triggerPortal.TriggerObjectID));
                    break;

                case IMS2TriggerActor triggerActor:
                    mapEntity.TriggerActors.Add(new(triggerActor.TriggerObjectID, triggerActor.IsVisible, triggerActor.InitialSequence));
                    break;

                case IMS2TriggerCube triggerCube:
                    mapEntity.TriggerCubes.Add(new(triggerCube.TriggerObjectID, triggerCube.IsVisible));
                    break;

                case IMS2TriggerSound triggerSound:
                    mapEntity.TriggerSounds.Add(new(triggerSound.TriggerObjectID, triggerSound.Enabled));
                    break;

                case IMS2TriggerSkill triggerSkill:
                    mapEntity.TriggerSkills.Add(new(triggerSkill.TriggerObjectID, triggerSkill.skillID,
                                                    (short)triggerSkill.skillLevel, (byte)triggerSkill.count, CoordF.FromVector3(triggerSkill.Position)));
                    break;
                }

                break;

            case IMS2LiftableTargetBox liftableTargetBox:
                mapEntity.LiftableTargets.Add(new(liftableTargetBox.liftableTarget, CoordF.FromVector3(liftableTargetBox.Position),
                                                  CoordF.FromVector3(liftableTargetBox.ShapeDimensions)));
                break;

            case IMS2PhysXProp physXProp:
                switch (physXProp)
                {
                case IMS2CubeProp prop:
                    if (!prop.IsObjectWeapon)
                    {
                        break;
                    }

                    List <int> weaponIds = prop.ObjectWeaponItemCode.SplitAndParseToInt(',').ToList();
                    mapEntity.WeaponObjects.Add(new(CoordB.FromVector3(prop.Position), weaponIds));
                    break;

                case IMS2Liftable liftable:
                    mapEntity.LiftableObjects.Add(new(liftable.EntityId, (int)liftable.ItemID, liftable.ItemStackCount, liftable.MaskQuestID,
                                                      liftable.MaskQuestState, liftable.EffectQuestID, liftable.EffectQuestState, liftable.ItemLifeTime,
                                                      liftable.LiftableRegenCheckTime, liftable.LiftableFinishTime, CoordF.FromVector3(liftable.Position),
                                                      CoordF.FromVector3(liftable.Rotation)));
                    break;

                case IMS2Vibrate vibrate:
                    mapEntity.VibrateObjects.Add(new(vibrate.EntityId, CoordF.FromVector3(physXProp.Position)));
                    break;
                }

                break;
            }
        }

        BuildWaypoints(tempPatrolData, tempWaypoints, mapEntity);
    }