private void LoadLevels() { var levelsNode = m_treeView.Nodes.Add("Levels"); foreach (var path in Directory.EnumerateDirectories(Path.Combine(m_clientRoot, "levels"))) { using (var dir = new DirManager(path)) { if (dir.Exists("leveldata.xml")) { var n = levelsNode.Nodes.Add("level-viewer", Path.GetFileName(path), 5, 5); } } } levelsNode.Expand(); }
// generates navmesh data for the selected levels. public static void GenerateAllNav(WorldIdXmlLoader worldIdXmlLoader, DirManager meshesDir, DirManager levelsDir, string levelId, bool skipExistingNav, string outputPath) { int curFolderIndex = -1; foreach (var pairs in worldIdXmlLoader.FolderNamesById) { curFolderIndex++; string clientLevelId = pairs.Key; string levelFolder = pairs.Value; if (!string.IsNullOrEmpty(levelId) && levelId != clientLevelId) { // skip excluded continue; } string outputNavFilename = Path.Combine(outputPath, clientLevelId + ".nav"); if (skipExistingNav && File.Exists(outputNavFilename)) { Console.WriteLine($" ** Skipping (already exists): {clientLevelId} - {levelFolder}"); continue; } Console.WriteLine($" Loading meshes for {clientLevelId} - {levelFolder}"); var LEVEL_TIME = DateTime.Now; var TIMER = DateTime.Now; var geoSpace = new GeoSpace(); // brushes var brushlst = LevelLoadHelper.CreateBrushLstLoader(levelsDir, Path.Combine(levelFolder, "brush.lst")); if (brushlst != null) { var cgfMap = LevelLoadHelper.CreateBrushLstCgfLoaderMap(meshesDir, brushlst); foreach (var brush in brushlst.brushEntries) { CgfLoader cgf; if (!cgfMap.TryGetValue(brush.meshIdx, out cgf)) { continue; } Matrix brushMatrix = LevelLoadHelper.GetBrushMatrix(brush); AddCgfToWorld(cgf, ref brushMatrix, brush.position, geoSpace); } } // objects var ctx = LevelLoadHelper.LoadObjectsLst(meshesDir, levelsDir, levelFolder); if (ctx != null) { foreach (var o in ctx.objects) { CgfLoader cgf; if (!ctx.cgfMap.TryGetValue(o.ObjectId, out cgf)) { continue; } var xform = LevelLoadHelper.GetObjectMatrix(o); AddCgfToWorld(cgf, ref xform, o.Position, geoSpace); } } // terrain bool loadedH32 = false; string h32path = Path.Combine(levelFolder, @"terrain\land_map.h32"); if (levelsDir.Exists(h32path)) { using (var landMapStream = levelsDir.OpenFile(h32path)) new H32Loader(landMapStream).LoadIntoGeoSpace(geoSpace); loadedH32 = true; } Console.WriteLine(" Data load time: " + (DateTime.Now - TIMER)); if (brushlst == null && ctx == null && !loadedH32) { Console.WriteLine(" ** Skipping (no level data found)"); continue; } // build geo TIMER = DateTime.Now; geoSpace.BuildTree(); geoSpace.Validate(); Console.WriteLine(" Geo build time: " + (DateTime.Now - TIMER)); // get size of level for scanning var bb = geoSpace.GetBoundingBox(); float startX = Math.Max(0, bb.Min.X); float endX = bb.Max.X; float startY = Math.Max(0, bb.Min.Y); float endY = bb.Max.Y; int top = (int)Math.Ceiling(bb.Max.Z); int bot = Math.Max(0, (int)bb.Min.Z); // TODO - print bounding box // TODO - save log file if (endX <= startX || endY <= startY || top < bot) { throw new InvalidOperationException( $"unexpected level size for {clientLevelId} {levelFolder} bb: {bb}"); } // compile mesh TIMER = DateTime.Now; float step = 1f; var navMeshBuilder = new NavMeshBuilder(startX, startY, bot, endX, endY, top, step); CompiledNavMeshSet compiledMeshSet = navMeshBuilder.ScanFloor(geoSpace); Console.WriteLine(" Raycast time: " + (DateTime.Now - TIMER)); // save to file using (var fs = File.OpenWrite(outputNavFilename)) compiledMeshSet.Save(fs); Console.WriteLine($" Level {clientLevelId} finished in {DateTime.Now - LEVEL_TIME}"); } }
public void Process() { DateTime timer = DateTime.Now; if (!Directory.Exists(aionClientPath)) { throw new FileNotFoundException($"Aion client installation path [{aionClientPath}] doesn't exist or not a folder path"); } var meshesDir = new DirManager(aionClientPath, new[] { @"Levels\Common", @"objects\npc\event_object", @"objects\npc\level_object" }); // Read world_maps.xml and WorldId.xml and find Levels to process Console.WriteLine(" Generating available levels list..."); WorldIdXmlLoader worldIdXmlLoader = LoadWorldIdXml(); Console.WriteLine(" Done."); var worldGeoBuilders = new List <WorldGeoFileBuilder>(); var levelMeshData = new Dictionary <string, CgfMeshesToLoad>(); // key is level folder name Console.WriteLine(" Processing levels..."); bool containsValidLevel = false; int curFolderIndex = -1; // Load levels dir... load all except common. var subdirs = Directory.EnumerateDirectories(Path.Combine(aionClientPath, "Levels")) .Select(fulldir => Path.GetFileName(fulldir)) .Where(dir => !dir.Equals("common", StringComparison.InvariantCultureIgnoreCase)); var levelsDir = new DirManager(Path.Combine(aionClientPath, "Levels"), subdirs); // special case to extract the login level... // login is like any other level, except it has no ID and isn't included in WorldId.xml. if (levelId == "login") { Console.WriteLine("*** Extracting login level ***"); worldIdXmlLoader.FolderNamesById["login"] = "******"; } if (generateGeo) { foreach (var pairs in worldIdXmlLoader.FolderNamesById) { curFolderIndex++; string clientLevelId = pairs.Key; string levelFolder = pairs.Value; if (!string.IsNullOrEmpty(levelId) && levelId != clientLevelId) { // skip excluded continue; } Console.WriteLine($" [{clientLevelId}] - ({curFolderIndex}/{worldIdXmlLoader.FolderNamesById.Count}) - {levelFolder} ..."); Console.WriteLine(" Parsing leveldata.xml ..."); if (!levelsDir.Exists(Path.Combine(levelFolder, "leveldata.xml"))) { Console.WriteLine(" leveldata.xml not found, skipping level."); continue; } // Note: this list is referenced later by index. List <string> vegetationCgfFilenames; Point mapSize; using (var levelDataXml = levelsDir.OpenFile(Path.Combine(levelFolder, "leveldata.xml"))) { var levelData = new LevelDataXmlLoader(levelDataXml); vegetationCgfFilenames = levelData.VegetationCgfFilenames.Select(Util.NormalizeMeshFilename).ToList(); mapSize = levelData.MapWidthAndHeight; } var meshData = new CgfMeshesToLoad(); BrushLstLoader brushLst = null; if (levelsDir.Exists(Path.Combine(levelFolder, "brush.lst"))) { Console.WriteLine(" Parsing brush.lst ... "); using (var stream = levelsDir.OpenFile(Path.Combine(levelFolder, "brush.lst"))) brushLst = new BrushLstLoader(stream); // TODO - un-hardcode if (brushLst.m_eventUsage[1]) { Console.WriteLine(" * Supports event: X-Mas"); } if (brushLst.m_eventUsage[2]) { Console.WriteLine(" * Supports event: Halloween"); } if (brushLst.m_eventUsage[3]) { Console.WriteLine(" * Supports event: Brax Cafe"); } if (brushLst.m_eventUsage[4]) { Console.WriteLine(" * Supports event: Valentines"); } } else { Console.WriteLine(" brush.lst not found, skipping"); } List <ObjectsLstItem> objectsLst = null; if (levelsDir.Exists(Path.Combine(levelFolder, "objects.lst"))) { Console.WriteLine(" Parsing objects.lst ... "); using (var stream = levelsDir.OpenFile(Path.Combine(levelFolder, "objects.lst"))) objectsLst = ObjectsLstLoader.Load(stream, mapSize.X, mapSize.Y); } else { Console.WriteLine(" objects.lst not found, skipping"); } // ------------------------------ meshData.meshFiles = new List <string>(); // brushes if (brushLst != null) { meshData.meshFiles.AddRange(brushLst.brushInfoList.Select(o => o.filename)); } // vegetation meshData.meshFiles.AddRange(vegetationCgfFilenames); // normalize names and dedupe. example entry: "levels/common/dark/natural/rocks/base/na_d_rockgngrass_05a.cgf" meshData.meshFiles = meshData.meshFiles.Select(Util.NormalizeMeshFilename).Distinct().ToList(); meshData.meshUsage = new int[meshData.meshFiles.Count]; if (!noMesh) { levelMeshData.Add(levelFolder, meshData); } byte[] landMapH32 = null; if (levelsDir.Exists(Path.Combine(levelFolder, @"terrain\land_map.h32"))) { using (var stream = levelsDir.OpenFile(Path.Combine(levelFolder, @"terrain\land_map.h32"))) { using (var ms = new MemoryStream()) { stream.CopyTo(ms); landMapH32 = ms.ToArray(); } } } // keep track of all required cgfs if (brushLst != null) { m_requiredCgfs.UnionWith(brushLst.brushInfoList.Select(o => Util.NormalizeMeshFilename(o.filename))); } if (objectsLst != null) { m_requiredCgfs.UnionWith(vegetationCgfFilenames.Select(Util.NormalizeMeshFilename)); } // level data must be loaded first to find the required cgfs. then, cgfs must be loaded to determine if // they contain collision data or not. then we can write the geo file minus non-collidable meshes. var w = new WorldGeoFileBuilder(clientLevelId, landMapH32, brushLst, vegetationCgfFilenames, objectsLst); // these will be processed after meshs worldGeoBuilders.Add(w); containsValidLevel = true; Console.WriteLine(" Done."); } Console.WriteLine(" Done."); // -------------------------------------------------------------------------------- if (!noMesh && containsValidLevel) { Console.WriteLine(" Generating meshs.geo ..."); int meshesSaved = 0; string meshesGeoFile = Path.Combine(outputPath, "meshs.geo"); using (var meshesGeoDataStream = new BinaryWriter(File.Open(meshesGeoFile, FileMode.Create))) { foreach (string s in m_requiredCgfs) { string cgfPath = PakUtil.NormalizeFilename(s); if (!meshesDir.Exists(cgfPath)) { Console.WriteLine(" Cgf not found: " + cgfPath); continue; } using (var cgfStream = meshesDir.OpenFile(cgfPath)) AddToMeshesGeo(cgfPath, new CgfLoader(cgfStream), meshesGeoDataStream, 1 /*collisionIntention=physical*/); meshesSaved++; } } Console.WriteLine(" Done. " + meshesSaved + "/" + m_requiredCgfs.Count + " meshes saved."); // ----------------------------------- Console.WriteLine(" Writing world.geo files ..."); int wc = 0; foreach (var w in worldGeoBuilders) { wc++; Console.Write($" Creating {w.ClientLevelId}.geo file [{wc}/{worldGeoBuilders.Count}]... "); w.CreateWorldGeoFile(outputPath, noH32, m_loadedCgfs, m_emptyCgfs); Console.WriteLine(" Done."); } Console.WriteLine(" Done."); } // ------------------------------------ /*VERBOSE Console.WriteLine(" Check meshes that were not found ..."); * foreach (var levelMeshes in levelMeshData) * { * Console.WriteLine(" " + levelMeshes.Key); * CgfMeshesToLoad brushLstMeshData = levelMeshes.Value; * for (int i = 0; i < brushLstMeshData.meshFiles.Count; i++) * { * if (brushLstMeshData.meshUsage[i] == 0 && !m_emptyCgfs.Contains(Util.NormalizeMeshFilename(brushLstMeshData.meshFiles[i]))) * { * Console.WriteLine(" " + brushLstMeshData.meshFiles[i]); * } * } * }*/ Console.WriteLine(" Done."); } // -------------------------------------------------------------------------------- if (generateDoors) { Console.WriteLine("Generating door mesh data..."); // Writes 2 files: // - door data, pairing level+entityid to position // - door mesh file, containing start and end variations of each door model. // Original AL stores models in data\geo\model\static_doors as .cga files. // These are not actually .cga files, but instead are the same format as .geo files. // These meshes only contained a single state. // // This following code generates door data in a new format: // - Each door model is loaded twice, in the start and end positions. // - Each model has _start or _end appended to the mesh name. // - All unique door meshes are stored in doors.geo, following the same format as // meshes.geo. Meshes should be identified by original name + _start or _end. // - All door instances are stored in doors.dat and define world placement. // static_doors.xml is still necessary as it provides unique info such as key item id. var doorModelNames = new HashSet <string>(); using (var doorsDat = new BinaryWriter(File.Open(Path.Combine(outputPath, "doors.dat"), FileMode.Create))) { doorsDat.Write(0x524F4F44); // collect unique doors foreach (var pairs in worldIdXmlLoader.FolderNamesById) { curFolderIndex++; string clientLevelId = pairs.Key; string levelFolder = pairs.Value; if (!string.IsNullOrEmpty(levelId) && levelId != clientLevelId) { // skip excluded continue; } Console.WriteLine($" [{clientLevelId}] - ({curFolderIndex}/{worldIdXmlLoader.FolderNamesById.Count}) - {levelFolder} ..."); List <DoorInfo> doorInfos; try { doorInfos = DoorLoader.LoadDoorInfosForLevel(levelsDir, levelFolder); } catch { Console.WriteLine(" Level folder not found."); continue; } foreach (var door in doorInfos) { string meshName = PakUtil.NormalizeFilename(door.object_AnimatedModel); doorModelNames.Add(meshName); doorsDat.Write((short)meshName.Length); byte[] meshFileNameBytes = Encoding.ASCII.GetBytes(meshName); doorsDat.Write(meshFileNameBytes); doorsDat.Write(int.Parse(clientLevelId)); doorsDat.Write(door.EntityId); doorsDat.Write(door.Pos.X); doorsDat.Write(door.Pos.Y); doorsDat.Write(door.Pos.Z); // TODO investigate dir / use_dir. value correlates to z angle... doorsDat.Write(door.Angles.X); doorsDat.Write(door.Angles.Y); doorsDat.Write(door.Angles.Z); } } } m_loadedCgfs = new HashSet <string>(); m_emptyCgfs = new HashSet <string>(); // generate door meshes. // a door model may be referenced many times, so models are saved separately from instances. int meshesSaved = 0; string doorsGeoFile = Path.Combine(outputPath, "doors.geo"); using (var doorMeshesGeoDataStream = new BinaryWriter(File.Open(doorsGeoFile, FileMode.Create))) { foreach (var cgfPath in doorModelNames.OrderBy(o => o)) { if (!meshesDir.Exists(cgfPath)) { Console.WriteLine(" Door Cgf/Cga not found: " + cgfPath); continue; } // TODO - parameterize m_loadedCgfs and m_emptyCgfs... using (var cgfStream = meshesDir.OpenFile(cgfPath)) { var cgfFirstState = new CgfLoader(cgfStream); var cgfSecondState = cgfFirstState.CloneAtTime(999999); byte collisionIntention = 1 + (1 << 4); // physical + door AddToMeshesGeo(cgfPath + "_start", cgfFirstState, doorMeshesGeoDataStream, collisionIntention); AddToMeshesGeo(cgfPath + "_end", cgfSecondState, doorMeshesGeoDataStream, collisionIntention); } meshesSaved++; } } // doors should have collision data, otherwise they won't be very effective. foreach (var empty in m_emptyCgfs) { Console.WriteLine("Warning: door has no collision data: " + empty); } Console.WriteLine("Door meshes: " + meshesSaved); } // -------------------------------------------------------------------------------- if (generateNav) { Console.WriteLine(" Generating NavMesh .nav data..."); var start = DateTime.Now; NavMeshProcessor.GenerateAllNav(worldIdXmlLoader, meshesDir, levelsDir, levelId, skipExistingNav, outputPath); Console.WriteLine(" NavMesh processing time: " + (DateTime.Now - start)); } TimeSpan timerEnd = DateTime.Now - timer; Console.WriteLine(" Processing time: " + timerEnd); }