Jump to content

Recommended Posts

Hey everyone,

I’ve spent days trying to get an AI to write the code I need, but honestly, I’ve run out of patience and motivation — it just doesn’t work the way I want it to.

What the script does:
It’s a MapSorter that I want to include in my ForeverMetin2 tool.
The script is supposed to sort everything used and placed on a map, such as:

textures

skybox

objects

Here’s what’s not working properly:
The program does find the following:

textures → but moves them to the ymir work/ folder

property files → also moved to ymir work/

environment

terrainmaps

The problem is that it doesn’t move the files to the folders I actually want — specifically, to the directories where they were originally found.

It also places everything in
my_map/my_map_finished/
instead of in the same directory where my tool (Forevermetin2.exe) is located, e.g.
Forevermetin2.exe
*mymap*_finished/

Additionally, the script doesn’t search inside the found property files for specific strings that lead to things like:

zone

tree

effect
...and similar elements.

If you understand what I’m trying to achieve and think you can help, please feel free to reach out. I’m happy to offer a small payment (e.g. PSC) as a thank-you. The tool is meant not just for me but for the whole community.

Thanks in advance!

Script:
 

Spoiler
using script;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

public static class Worker
{
    private static readonly Regex TexRegex =
        new Regex(@"\.(dds|png|jpeg|jpg|tga)\b", RegexOptions.IgnoreCase);

    public static void Run(string mapRoot, Action<int> reportProgress = null)
    {
        if (!mapRoot.EndsWith("\\") && !mapRoot.EndsWith("/"))
            mapRoot += "/";

        string ymRoot = @"d:/ymir work/";
        string mapName = Path.GetFileName(Path.GetDirectoryName(mapRoot));
        string outRoot = Path.Combine(Path.GetDirectoryName(mapRoot), mapName + "_finished");
        Directory.CreateDirectory(outRoot);

        string settingFile = FastFindFile(mapRoot, "setting.txt");
        if (settingFile == null) throw new FileNotFoundException("setting.txt nicht gefunden in " + mapRoot);

        string texSetLine = File.ReadLines(settingFile)
            .First(l => l.StartsWith("TextureSet", StringComparison.OrdinalIgnoreCase));
        string texSetRel = texSetLine.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries)[1]
                                     .Replace('\\', '/');
        string texSetAbs = Path.Combine(ymRoot, texSetRel);
        if (!File.Exists(texSetAbs)) throw new FileNotFoundException("TextureSet nicht gefunden: " + texSetAbs);
        CopyWithTree(texSetAbs, outRoot, ymRoot);
        reportProgress?.Invoke(10);

        foreach (var t in ExtractPaths(File.ReadAllText(texSetAbs)))
            CopyRecursive(t, outRoot, ymRoot, new HashSet<string>());
        reportProgress?.Invoke(20);

        string envLine = File.ReadLines(settingFile)
            .First(l => l.StartsWith("Environment", StringComparison.OrdinalIgnoreCase));
        string envRel = envLine.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries)[1]
                               .Replace('\\', '/');
        string envAbs = Directory.EnumerateFiles(Path.Combine(ymRoot, "environment"),
                                                  Path.GetFileName(envRel), SearchOption.AllDirectories)
                                  .FirstOrDefault();
        if (envAbs == null) throw new FileNotFoundException($".msenv-Datei {envRel} nicht gefunden!");
        CopyWithTree(envAbs, outRoot, ymRoot);
        reportProgress?.Invoke(30);

        string envContent = File.ReadAllText(envAbs);
        foreach (var l in ExtractPaths(envContent)) CopyRecursive(l, outRoot, ymRoot, new HashSet<string>());
        foreach (var m in ExtractZonePaths(envContent)) CopyRecursive(m, outRoot, ymRoot, new HashSet<string>());
        reportProgress?.Invoke(50);

        var ids = new HashSet<string>();
        foreach (var file in Directory.EnumerateFiles(mapRoot, "*.txt", SearchOption.AllDirectories)
                                      .Where(n => n.EndsWith("AreaData.txt", StringComparison.OrdinalIgnoreCase) ||
                                                  n.EndsWith("AreaAmbienceData.txt", StringComparison.OrdinalIgnoreCase)))
            ids.UnionWith(ExtractIds(File.ReadAllText(file)));

        string propRoot = Path.Combine(ymRoot, "property");
        var usedIds = new HashSet<string>();
        var doneLinks = new HashSet<string>();
        int hitFile = 0, zoneHit = 0;

        foreach (var pf in Directory.EnumerateFiles(propRoot, "*.*", SearchOption.AllDirectories)
                            .Where(f => f.EndsWith(".prb") || f.EndsWith(".prd") ||
                                        f.EndsWith(".pra") || f.EndsWith(".prt") ||
                                        f.EndsWith(".pre")))
        {
            try
            {
                string content = File.ReadAllText(pf);
                Console.WriteLine($"[DEBUG] Lese {pf}");
                Console.WriteLine($"[DEBUG] erste 3 Zeilen:\n{string.Join("\n", content.Split('\n').Take(3))}");
                string foundId = ids.FirstOrDefault(id => content.Contains(id));
                if (foundId == null || usedIds.Contains(foundId)) continue;
                usedIds.Add(foundId);

                hitFile++;
                CopyRecursive(pf, outRoot, ymRoot, doneLinks);

                bool isTabbed = pf.EndsWith(".pre", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prd", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".pra", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prt", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prb", StringComparison.OrdinalIgnoreCase);   // <- hinzu
                var links = isTabbed ? ExtractTabbedPaths(content, ymRoot)
                     : ExtractPropertyLinks(content, ymRoot);

                Console.WriteLine($"[DEBUG] Datei: {pf}");
                Console.WriteLine($"[DEBUG] isTabbed: {isTabbed}");
                Console.WriteLine($"[DEBUG] Anzahl Links: {links.Count}");
                foreach (var l in links) Console.WriteLine($"  -> {l}");

                foreach (var link in links)
                {
                    if (doneLinks.Contains(link)) continue;
                    doneLinks.Add(link);
                    CopyRecursive(link, outRoot, ymRoot, doneLinks);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[CRASH] Datei: {pf}");
                Console.WriteLine($"[CRASH] Grund: {ex.Message}");
                Console.WriteLine($"[CRASH] Stack: {ex.StackTrace}");
                throw;   // damit Visual Studio sofort hält
            }
        }

        reportProgress?.Invoke(100);
        Console.WriteLine($">>> Property-Dateien (nur mit ID) kopiert: {hitFile}");
        Console.WriteLine($">>> Verlinkte interne Dateien kopiert: {zoneHit}");
    }

    /* -------------------------------------------------- */
    /* Helper Methods                                     */
    /* -------------------------------------------------- */

    private static List<string> ExtractPaths(string content)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(dds|png|jpeg|jpg|tga))[""']", RegexOptions.IgnoreCase))
            if (File.Exists(m.Groups[1].Value)) set.Add(m.Groups[1].Value);
        foreach (Match m in Regex.Matches(content, @"[a-zA-Z]:\\[^\s]+\.(dds|png|jpeg|jpg|tga)\b", RegexOptions.IgnoreCase))
            if (File.Exists(m.Value)) set.Add(m.Value);
        return set.ToList();
    }

    private static List<string> ExtractZonePaths(string content)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(gr2|spt|mse|mde|wav))[""']", RegexOptions.IgnoreCase))
            if (File.Exists(m.Groups[1].Value)) set.Add(m.Groups[1].Value);
        return set.ToList();
    }

    private static HashSet<string> ExtractIds(string content)
    {
        var idSet = new HashSet<string>();
        foreach (Match m in Regex.Matches(content, @"^\s*(\d{8,})\s*$", RegexOptions.Multiline))
            idSet.Add(m.Groups[1].Value);
        return idSet;
    }

    private static List<string> ExtractPropertyLinks(string content, string ymRoot)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        // 1) relative Pfade mit führendem Slash
        foreach (Match m in Regex.Matches(content,
                @"[\x00-\x1F\x22\x27]?(/[^\x00-\x1F\x22\x27]+\.(gr2|mde|mse|spt|wav))\b",
                RegexOptions.IgnoreCase))
        {
            string rel = m.Groups[1].Value.Replace('/', '\\');
            string abs = Path.Combine(ymRoot, rel.TrimStart('\\'));
            if (!File.Exists(abs))
                abs = FastFindFile(ymRoot, Path.GetFileName(abs));
            if (!string.IsNullOrEmpty(abs) && File.Exists(abs))
                set.Add(abs);
        }

        // 2) nur Dateiname -> in bekannten Unterordnern suchen
        foreach (Match m in Regex.Matches(content, @"[\x00-\x1F\x22\x27]([^\x00-\x1F\x22\x27/\\]+\.(gr2|mde|mse|spt|wav))\b", RegexOptions.IgnoreCase))
        {
            string name = m.Groups[1].Value;
            string found = FastFindFile(Path.Combine(ymRoot, "effect"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "tree"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "zone"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "sound"), name);
            if (found != null) set.Add(found);
        }

        return set.ToList();
    }

    private static void CopyWithTree(string file, string outRoot, string strip)
    {
        string rel = file.Substring(strip.Length).TrimStart('\\', '/');
        string tgt = Path.Combine(outRoot, "ymir work", rel);
        Directory.CreateDirectory(Path.GetDirectoryName(tgt));
        File.Copy(file, tgt, true);
    }

    private static string FastFindFile(string root, string name)
        => Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)
                    .FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase));

    private static void CopyRecursive(string file, string outRoot, string ymRoot, HashSet<string> done)
    {
        if (done.Contains(file)) return;
        if (!File.Exists(file))
        {
            if (TexRegex.IsMatch(file)) CopyAlternativeTexture(file, outRoot, ymRoot);
            return;
        }
        done.Add(file);

        string rel = file.Substring(ymRoot.Length).TrimStart('\\', '/');
        string tgt = Path.Combine(outRoot, "ymir work", rel);
        Directory.CreateDirectory(Path.GetDirectoryName(tgt));
        File.Copy(file, tgt, true);

        string content = File.ReadAllText(file);
        foreach (var t in ExtractPaths(content)) CopyRecursive(t, outRoot, ymRoot, done);
        if (file.EndsWith(".mse", StringComparison.OrdinalIgnoreCase))
            foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.mde)[""']", RegexOptions.IgnoreCase))
                CopyRecursive(m.Groups[1].Value, outRoot, ymRoot, done);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(gr2|spt|mse|mde))[""']", RegexOptions.IgnoreCase))
            CopyRecursive(m.Groups[1].Value, outRoot, ymRoot, done);
    }

    private static void CopyAlternativeTexture(string missingFile, string outRoot, string ymRoot)
    {
        string dir = Path.GetDirectoryName(missingFile);
        string name = Path.GetFileNameWithoutExtension(missingFile);
        foreach (var ext in new[] { ".dds", ".png", ".jpeg", ".jpg", ".tga" })
        {
            string candidate = Path.Combine(dir, name + ext);
            if (!File.Exists(candidate)) continue;
            string rel = candidate.Substring(ymRoot.Length).TrimStart('\\', '/');
            string tgt = Path.Combine(outRoot, "ymir work", rel);
            Directory.CreateDirectory(Path.GetDirectoryName(tgt));
            File.Copy(candidate, tgt, true);
            return;
        }
    }

    /* -------------------------------------------------- */
    /*  Extrahiert Tab-getrennte Pfade aus .pre/.prd/.pra/.prt  */
    /* -------------------------------------------------- */
    private static List<string> ExtractTabbedPaths(string content, string ymRoot)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (string.IsNullOrEmpty(content)) return set.ToList();

        var lines = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var rawLine in lines)
        {
            // typische .pre/.prd: Spalten sind per Tab getrennt; aber defensiv auch mit mehrere Leerzeichen behandeln
            var parts = rawLine.Split('\t');
            if (parts.Length < 2)
            {
                parts = Regex.Split(rawLine.Trim(), @"\s{2,}"); // fallback
                if (parts.Length < 2) continue;
            }

            string key = parts[0].Trim().ToLowerInvariant();
            string value = parts[1].Trim().Trim('"', '\'');

            // nur relevante Keys (wie vorher)
            if (!key.Contains("file") && !key.Contains("mesh") && !key.Contains("effect") &&
                !key.Contains("model") && !key.Contains("sound")) continue;

            // 1) absolute path in value?
            var mAbs = Regex.Match(value, @"^[a-zA-Z]:\\.*\.(gr2|mde|mse|spt|wav)$", RegexOptions.IgnoreCase);
            if (mAbs.Success)
            {
                var abs = mAbs.Value;
                if (File.Exists(abs)) set.Add(Path.GetFullPath(abs));
                continue;
            }

            // 2) path with forward slashes or starting slash => map to ymRoot
            var mSlash = Regex.Match(value, @"^/?(.+?\.(gr2|mde|mse|spt|wav))$", RegexOptions.IgnoreCase);
            if (mSlash.Success)
            {
                var candidate = Path.Combine(ymRoot, mSlash.Groups[1].Value.Replace('/', Path.DirectorySeparatorChar));
                if (File.Exists(candidate)) { set.Add(Path.GetFullPath(candidate)); continue; }

                // fallback: suche Datei im ymRoot
                var found = FastFindFile(ymRoot, Path.GetFileName(candidate));
                if (found != null) set.Add(found);
                continue;
            }

            // 3) wenn value nur Dateiname -> suche in bekannten Ordnern
            var nameOnly = Regex.Match(value, @"^[^\\/:]+?\.(gr2|mde|mse|spt|wav)$", RegexOptions.IgnoreCase);
            if (nameOnly.Success)
            {
                string name = nameOnly.Value;
                string found = FastFindFile(Path.Combine(ymRoot, "effect"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "tree"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "zone"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "sound"), name)
                             ?? FastFindFile(ymRoot, name); // global fallback
                if (found != null) set.Add(found);
                continue;
            }
        }

        return set.ToList();
    }

}

 

 

  • Flame 1
  • Developer

Not mentioned but required:

  • .gr2 -> 3D models used by props and buildings
  • .mdatr -> metadata paired with .gr2
  • .spt -> SpeedTree models for trees
  • effect/ -> particle and visual effects referenced by props or environment
  • sound/ -> ambient sounds and reverb zones
  • .msenv so skybox/ -> sky textures used in the environment
  • probably more
  • ++ don't forget we're using CRC32 table to get some prop files, copy the table.

I wrote a version in Python, because it's definitely the best choice for vibecoding, but without realizing that I would have to use such a granny bind to be able to extract the texture lists from the .gr2 files I would export,

You would encounter the same problem doing it in C#. You need something that parses the .gr2 files, extracting the names of the texture files it uses.

 

Since I had already done it in Python and everything worked except for extracting the texture files related to gr2, I made a small C++ console app to take care of it, which I still call from Python, rofl.

The result at this point is something that seems functional, even if it's ugly. I would rewrite it in C++, but I needed it quickly one night because some of the team couldn't find all the files used in a map.

Honestly, many people are definitely looking for a tool like this, but I know for a fact that it's already quite widespread, and I believe that some mappers have already paid developers in the past to make one to keep for themselves, so I don't know how nice it would be to release something like this.

 

It created a _TUTORIAL{mapname} folder with all the files used by the map, with the exact directory

 

Edited by Mitachi
  • Metin2 Dev 1
  • Love 2

Villains are not born, they are made.
Join

Hello Mitachi, 
thank you for your comment, your help, and your effort. 

I saw it's not possible to get someone to create one for 
the community that actually works 100%. Let's leave "tree" 
aside, since many trees have textures that somehow don't 
actually exist, yet have visible textures; no tool can do 
that. But Zone, MSE, MDE, and everything else should be doable. 

The script I showed here does its job "okay" but doesn't export 
the Zone files. Chrisdev's map sorter, for example, even exports 
all textures, but when it comes to ".mse," the ".mde" file, which 
can be located within an MSE, is completely ignored. 

I think I'll just go back to the old way, writing down every object 
I used and leaving the tools alone. Yours at least looks functional, even if 
the .gr files don't have any textures, but it's still usable. 

Thank you.

  • 1 month later...
On 10/13/2025 at 9:51 PM, Ace said:

Hey everyone,

I’ve spent days trying to get an AI to write the code I need, but honestly, I’ve run out of patience and motivation — it just doesn’t work the way I want it to.

What the script does:
It’s a MapSorter that I want to include in my ForeverMetin2 tool.
The script is supposed to sort everything used and placed on a map, such as:

textures

skybox

objects

Here’s what’s not working properly:
The program does find the following:

textures → but moves them to the ymir work/ folder

property files → also moved to ymir work/

environment

terrainmaps

The problem is that it doesn’t move the files to the folders I actually want — specifically, to the directories where they were originally found.

It also places everything in
my_map/my_map_finished/
instead of in the same directory where my tool (Forevermetin2.exe) is located, e.g.
Forevermetin2.exe
*mymap*_finished/

Additionally, the script doesn’t search inside the found property files for specific strings that lead to things like:

zone

tree

effect
...and similar elements.

If you understand what I’m trying to achieve and think you can help, please feel free to reach out. I’m happy to offer a small payment (e.g. PSC) as a thank-you. The tool is meant not just for me but for the whole community.

Thanks in advance!

Script:
 

  Reveal hidden contents
using script;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

public static class Worker
{
    private static readonly Regex TexRegex =
        new Regex(@"\.(dds|png|jpeg|jpg|tga)\b", RegexOptions.IgnoreCase);

    public static void Run(string mapRoot, Action<int> reportProgress = null)
    {
        if (!mapRoot.EndsWith("\\") && !mapRoot.EndsWith("/"))
            mapRoot += "/";

        string ymRoot = @"d:/ymir work/";
        string mapName = Path.GetFileName(Path.GetDirectoryName(mapRoot));
        string outRoot = Path.Combine(Path.GetDirectoryName(mapRoot), mapName + "_finished");
        Directory.CreateDirectory(outRoot);

        string settingFile = FastFindFile(mapRoot, "setting.txt");
        if (settingFile == null) throw new FileNotFoundException("setting.txt nicht gefunden in " + mapRoot);

        string texSetLine = File.ReadLines(settingFile)
            .First(l => l.StartsWith("TextureSet", StringComparison.OrdinalIgnoreCase));
        string texSetRel = texSetLine.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries)[1]
                                     .Replace('\\', '/');
        string texSetAbs = Path.Combine(ymRoot, texSetRel);
        if (!File.Exists(texSetAbs)) throw new FileNotFoundException("TextureSet nicht gefunden: " + texSetAbs);
        CopyWithTree(texSetAbs, outRoot, ymRoot);
        reportProgress?.Invoke(10);

        foreach (var t in ExtractPaths(File.ReadAllText(texSetAbs)))
            CopyRecursive(t, outRoot, ymRoot, new HashSet<string>());
        reportProgress?.Invoke(20);

        string envLine = File.ReadLines(settingFile)
            .First(l => l.StartsWith("Environment", StringComparison.OrdinalIgnoreCase));
        string envRel = envLine.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries)[1]
                               .Replace('\\', '/');
        string envAbs = Directory.EnumerateFiles(Path.Combine(ymRoot, "environment"),
                                                  Path.GetFileName(envRel), SearchOption.AllDirectories)
                                  .FirstOrDefault();
        if (envAbs == null) throw new FileNotFoundException($".msenv-Datei {envRel} nicht gefunden!");
        CopyWithTree(envAbs, outRoot, ymRoot);
        reportProgress?.Invoke(30);

        string envContent = File.ReadAllText(envAbs);
        foreach (var l in ExtractPaths(envContent)) CopyRecursive(l, outRoot, ymRoot, new HashSet<string>());
        foreach (var m in ExtractZonePaths(envContent)) CopyRecursive(m, outRoot, ymRoot, new HashSet<string>());
        reportProgress?.Invoke(50);

        var ids = new HashSet<string>();
        foreach (var file in Directory.EnumerateFiles(mapRoot, "*.txt", SearchOption.AllDirectories)
                                      .Where(n => n.EndsWith("AreaData.txt", StringComparison.OrdinalIgnoreCase) ||
                                                  n.EndsWith("AreaAmbienceData.txt", StringComparison.OrdinalIgnoreCase)))
            ids.UnionWith(ExtractIds(File.ReadAllText(file)));

        string propRoot = Path.Combine(ymRoot, "property");
        var usedIds = new HashSet<string>();
        var doneLinks = new HashSet<string>();
        int hitFile = 0, zoneHit = 0;

        foreach (var pf in Directory.EnumerateFiles(propRoot, "*.*", SearchOption.AllDirectories)
                            .Where(f => f.EndsWith(".prb") || f.EndsWith(".prd") ||
                                        f.EndsWith(".pra") || f.EndsWith(".prt") ||
                                        f.EndsWith(".pre")))
        {
            try
            {
                string content = File.ReadAllText(pf);
                Console.WriteLine($"[DEBUG] Lese {pf}");
                Console.WriteLine($"[DEBUG] erste 3 Zeilen:\n{string.Join("\n", content.Split('\n').Take(3))}");
                string foundId = ids.FirstOrDefault(id => content.Contains(id));
                if (foundId == null || usedIds.Contains(foundId)) continue;
                usedIds.Add(foundId);

                hitFile++;
                CopyRecursive(pf, outRoot, ymRoot, doneLinks);

                bool isTabbed = pf.EndsWith(".pre", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prd", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".pra", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prt", StringComparison.OrdinalIgnoreCase) ||
                pf.EndsWith(".prb", StringComparison.OrdinalIgnoreCase);   // <- hinzu
                var links = isTabbed ? ExtractTabbedPaths(content, ymRoot)
                     : ExtractPropertyLinks(content, ymRoot);

                Console.WriteLine($"[DEBUG] Datei: {pf}");
                Console.WriteLine($"[DEBUG] isTabbed: {isTabbed}");
                Console.WriteLine($"[DEBUG] Anzahl Links: {links.Count}");
                foreach (var l in links) Console.WriteLine($"  -> {l}");

                foreach (var link in links)
                {
                    if (doneLinks.Contains(link)) continue;
                    doneLinks.Add(link);
                    CopyRecursive(link, outRoot, ymRoot, doneLinks);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[CRASH] Datei: {pf}");
                Console.WriteLine($"[CRASH] Grund: {ex.Message}");
                Console.WriteLine($"[CRASH] Stack: {ex.StackTrace}");
                throw;   // damit Visual Studio sofort hält
            }
        }

        reportProgress?.Invoke(100);
        Console.WriteLine($">>> Property-Dateien (nur mit ID) kopiert: {hitFile}");
        Console.WriteLine($">>> Verlinkte interne Dateien kopiert: {zoneHit}");
    }

    /* -------------------------------------------------- */
    /* Helper Methods                                     */
    /* -------------------------------------------------- */

    private static List<string> ExtractPaths(string content)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(dds|png|jpeg|jpg|tga))[""']", RegexOptions.IgnoreCase))
            if (File.Exists(m.Groups[1].Value)) set.Add(m.Groups[1].Value);
        foreach (Match m in Regex.Matches(content, @"[a-zA-Z]:\\[^\s]+\.(dds|png|jpeg|jpg|tga)\b", RegexOptions.IgnoreCase))
            if (File.Exists(m.Value)) set.Add(m.Value);
        return set.ToList();
    }

    private static List<string> ExtractZonePaths(string content)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(gr2|spt|mse|mde|wav))[""']", RegexOptions.IgnoreCase))
            if (File.Exists(m.Groups[1].Value)) set.Add(m.Groups[1].Value);
        return set.ToList();
    }

    private static HashSet<string> ExtractIds(string content)
    {
        var idSet = new HashSet<string>();
        foreach (Match m in Regex.Matches(content, @"^\s*(\d{8,})\s*$", RegexOptions.Multiline))
            idSet.Add(m.Groups[1].Value);
        return idSet;
    }

    private static List<string> ExtractPropertyLinks(string content, string ymRoot)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        // 1) relative Pfade mit führendem Slash
        foreach (Match m in Regex.Matches(content,
                @"[\x00-\x1F\x22\x27]?(/[^\x00-\x1F\x22\x27]+\.(gr2|mde|mse|spt|wav))\b",
                RegexOptions.IgnoreCase))
        {
            string rel = m.Groups[1].Value.Replace('/', '\\');
            string abs = Path.Combine(ymRoot, rel.TrimStart('\\'));
            if (!File.Exists(abs))
                abs = FastFindFile(ymRoot, Path.GetFileName(abs));
            if (!string.IsNullOrEmpty(abs) && File.Exists(abs))
                set.Add(abs);
        }

        // 2) nur Dateiname -> in bekannten Unterordnern suchen
        foreach (Match m in Regex.Matches(content, @"[\x00-\x1F\x22\x27]([^\x00-\x1F\x22\x27/\\]+\.(gr2|mde|mse|spt|wav))\b", RegexOptions.IgnoreCase))
        {
            string name = m.Groups[1].Value;
            string found = FastFindFile(Path.Combine(ymRoot, "effect"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "tree"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "zone"), name) ??
                           FastFindFile(Path.Combine(ymRoot, "sound"), name);
            if (found != null) set.Add(found);
        }

        return set.ToList();
    }

    private static void CopyWithTree(string file, string outRoot, string strip)
    {
        string rel = file.Substring(strip.Length).TrimStart('\\', '/');
        string tgt = Path.Combine(outRoot, "ymir work", rel);
        Directory.CreateDirectory(Path.GetDirectoryName(tgt));
        File.Copy(file, tgt, true);
    }

    private static string FastFindFile(string root, string name)
        => Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)
                    .FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase));

    private static void CopyRecursive(string file, string outRoot, string ymRoot, HashSet<string> done)
    {
        if (done.Contains(file)) return;
        if (!File.Exists(file))
        {
            if (TexRegex.IsMatch(file)) CopyAlternativeTexture(file, outRoot, ymRoot);
            return;
        }
        done.Add(file);

        string rel = file.Substring(ymRoot.Length).TrimStart('\\', '/');
        string tgt = Path.Combine(outRoot, "ymir work", rel);
        Directory.CreateDirectory(Path.GetDirectoryName(tgt));
        File.Copy(file, tgt, true);

        string content = File.ReadAllText(file);
        foreach (var t in ExtractPaths(content)) CopyRecursive(t, outRoot, ymRoot, done);
        if (file.EndsWith(".mse", StringComparison.OrdinalIgnoreCase))
            foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.mde)[""']", RegexOptions.IgnoreCase))
                CopyRecursive(m.Groups[1].Value, outRoot, ymRoot, done);
        foreach (Match m in Regex.Matches(content, @"[""']([a-zA-Z]:\\[^""']+\.(gr2|spt|mse|mde))[""']", RegexOptions.IgnoreCase))
            CopyRecursive(m.Groups[1].Value, outRoot, ymRoot, done);
    }

    private static void CopyAlternativeTexture(string missingFile, string outRoot, string ymRoot)
    {
        string dir = Path.GetDirectoryName(missingFile);
        string name = Path.GetFileNameWithoutExtension(missingFile);
        foreach (var ext in new[] { ".dds", ".png", ".jpeg", ".jpg", ".tga" })
        {
            string candidate = Path.Combine(dir, name + ext);
            if (!File.Exists(candidate)) continue;
            string rel = candidate.Substring(ymRoot.Length).TrimStart('\\', '/');
            string tgt = Path.Combine(outRoot, "ymir work", rel);
            Directory.CreateDirectory(Path.GetDirectoryName(tgt));
            File.Copy(candidate, tgt, true);
            return;
        }
    }

    /* -------------------------------------------------- */
    /*  Extrahiert Tab-getrennte Pfade aus .pre/.prd/.pra/.prt  */
    /* -------------------------------------------------- */
    private static List<string> ExtractTabbedPaths(string content, string ymRoot)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (string.IsNullOrEmpty(content)) return set.ToList();

        var lines = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var rawLine in lines)
        {
            // typische .pre/.prd: Spalten sind per Tab getrennt; aber defensiv auch mit mehrere Leerzeichen behandeln
            var parts = rawLine.Split('\t');
            if (parts.Length < 2)
            {
                parts = Regex.Split(rawLine.Trim(), @"\s{2,}"); // fallback
                if (parts.Length < 2) continue;
            }

            string key = parts[0].Trim().ToLowerInvariant();
            string value = parts[1].Trim().Trim('"', '\'');

            // nur relevante Keys (wie vorher)
            if (!key.Contains("file") && !key.Contains("mesh") && !key.Contains("effect") &&
                !key.Contains("model") && !key.Contains("sound")) continue;

            // 1) absolute path in value?
            var mAbs = Regex.Match(value, @"^[a-zA-Z]:\\.*\.(gr2|mde|mse|spt|wav)$", RegexOptions.IgnoreCase);
            if (mAbs.Success)
            {
                var abs = mAbs.Value;
                if (File.Exists(abs)) set.Add(Path.GetFullPath(abs));
                continue;
            }

            // 2) path with forward slashes or starting slash => map to ymRoot
            var mSlash = Regex.Match(value, @"^/?(.+?\.(gr2|mde|mse|spt|wav))$", RegexOptions.IgnoreCase);
            if (mSlash.Success)
            {
                var candidate = Path.Combine(ymRoot, mSlash.Groups[1].Value.Replace('/', Path.DirectorySeparatorChar));
                if (File.Exists(candidate)) { set.Add(Path.GetFullPath(candidate)); continue; }

                // fallback: suche Datei im ymRoot
                var found = FastFindFile(ymRoot, Path.GetFileName(candidate));
                if (found != null) set.Add(found);
                continue;
            }

            // 3) wenn value nur Dateiname -> suche in bekannten Ordnern
            var nameOnly = Regex.Match(value, @"^[^\\/:]+?\.(gr2|mde|mse|spt|wav)$", RegexOptions.IgnoreCase);
            if (nameOnly.Success)
            {
                string name = nameOnly.Value;
                string found = FastFindFile(Path.Combine(ymRoot, "effect"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "tree"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "zone"), name)
                             ?? FastFindFile(Path.Combine(ymRoot, "sound"), name)
                             ?? FastFindFile(ymRoot, name); // global fallback
                if (found != null) set.Add(found);
                continue;
            }
        }

        return set.ToList();
    }

}

 

 

i have this tool

spacer.png

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.