├── MinecraftVersionHistory
├── icon.ico
├── App.config
├── packages.config
├── Utilities
│ ├── JsonSorter
│ │ ├── BaseJsonSorter.cs
│ │ ├── MultiJsonSorter.cs
│ │ ├── PathedJsonSorter.cs
│ │ ├── SorterRequirements.cs
│ │ ├── JsonSorterFactory.cs
│ │ ├── NodeFinders.cs
│ │ ├── NodeMatchers.cs
│ │ └── JsonSorter.cs
│ ├── Profiler.cs
│ ├── CommandRunner.cs
│ ├── ProcessWrapper.cs
│ ├── GitRepo.cs
│ ├── Util.cs
│ └── NbtTranslationOptions.cs
├── Java
│ ├── MCP
│ │ ├── Mappings
│ │ │ ├── Sided.cs
│ │ │ ├── Mappings.cs
│ │ │ ├── FlatMap.cs
│ │ │ ├── MappedClass.cs
│ │ │ ├── MappingsIO.cs
│ │ │ └── Equivalencies.cs
│ │ ├── VersionedRenames.cs
│ │ └── RetroMCP.cs
│ ├── SnapshotSpec.cs
│ ├── JavaUpdater.cs
│ ├── JavaConfig.cs
│ └── JavaVersion.cs
├── Bedrock
│ ├── BedrockConfig.cs
│ ├── KeyMover.cs
│ ├── BedrockUpdater.cs
│ ├── BedrockVersion.cs
│ ├── PackMerger.cs
│ └── MergingSpec.cs
├── AppConfig.cs
├── Abstract
│ ├── Version.cs
│ ├── VersionConfig.cs
│ ├── VersionFacts.cs
│ └── Updater.cs
├── GlobalUsings.cs
├── MinecraftVersionHistory.csproj
├── Graph
│ ├── ReleaseBranch.cs
│ ├── VersionNode.cs
│ └── VersionGraph.cs
└── Program.cs
├── .gitmodules
├── MCPModernizer
├── Properties
│ └── launchSettings.json
├── MCPModernizer.csproj
├── ModernMCP.cs
├── MCP.cs
├── ClassicMCP.cs
└── Program.cs
├── .github
└── workflows
│ └── dotnet.yml
├── .gitattributes
├── MinecraftVersionHistory.sln
├── README.md
└── .gitignore
/MinecraftVersionHistory/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryashtar/minecraft-version-history/HEAD/MinecraftVersionHistory/icon.ico
--------------------------------------------------------------------------------
/MinecraftVersionHistory/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/BaseJsonSorter.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class BaseJsonSorter : IJsonSorter
4 | {
5 | public BaseJsonSorter()
6 | {
7 | }
8 |
9 | public abstract void Sort(JsonNode root);
10 | }
11 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "fNbt"]
2 | path = fNbt
3 | url = https://github.com/tryashtar/fNbt
4 | [submodule "utils.nbt"]
5 | path = utils.nbt
6 | url = https://github.com/tryashtar/utils.nbt
7 | [submodule "utils.utility"]
8 | path = utils.utility
9 | url = https://github.com/tryashtar/utils.utility
10 |
--------------------------------------------------------------------------------
/MCPModernizer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "MCP Modernizer": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--classic \"D:\\Minecraft\\Java Storage\\MCP\\Classic\" --modern-srg \"D:\\Minecraft\\Java Storage\\MCP\\Modern\\MCPConfig\\versions\" --modern-csv \"D:\\Minecraft\\Java Storage\\MCP\\Modern\\MCPMappingsArchive\" --output \"D:\\Minecraft\\Java Storage\\MCP\\Merged\""
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/Sided.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class Sided where T : new()
4 | {
5 | public readonly T Client;
6 | public readonly T Server;
7 | public Sided()
8 | {
9 | Client = new();
10 | Server = new();
11 | }
12 | public Sided(T client, T server)
13 | {
14 | Client = client;
15 | Server = server;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/MultiJsonSorter.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class MultiJsonSorter : BaseJsonSorter
4 | {
5 | public readonly IJsonSorter[] Sorters;
6 | public MultiJsonSorter(IEnumerable sorters) : base()
7 | {
8 | Sorters = sorters.ToArray();
9 | }
10 |
11 | public override void Sort(JsonNode root)
12 | {
13 | foreach (var sorter in Sorters)
14 | {
15 | sorter.Sort(root);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/BedrockConfig.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class BedrockConfig : VersionConfig
4 | {
5 | public readonly List PackMergers;
6 | public BedrockConfig(string folder, AppConfig parent, YamlMappingNode yaml) : base(folder, parent, yaml)
7 | {
8 | PackMergers = yaml.Go("pack merging").ToList(x => new PackMerger((YamlMappingNode)x)) ?? new();
9 | }
10 |
11 | protected override VersionFacts CreateVersionFacts(YamlMappingNode yaml)
12 | {
13 | return new VersionFacts(yaml);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MCPModernizer/MCPModernizer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | MCPModernizer
7 | enable
8 | enable
9 | x64
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/AppConfig.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class AppConfig
4 | {
5 | public readonly JavaConfig Java;
6 | public readonly BedrockConfig Bedrock;
7 | public readonly string GitInstallationPath;
8 | public readonly string GitIgnoreContents;
9 | public AppConfig(string folder, YamlMappingNode yaml)
10 | {
11 | GitInstallationPath = Util.FilePath(folder, yaml["git install"]);
12 | GitIgnoreContents = yaml.Go("gitignore").String();
13 | Java = new JavaConfig(folder, this, yaml["java"] as YamlMappingNode);
14 | Bedrock = new BedrockConfig(folder, this, yaml["bedrock"] as YamlMappingNode);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/PathedJsonSorter.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class PathedJsonSorter : BaseJsonSorter
4 | {
5 | public readonly INodeFinder Finder;
6 | private readonly NodeMatcher Matches;
7 | public PathedJsonSorter(INodeFinder finder, NodeMatcher matches) : base()
8 | {
9 | Finder = finder;
10 | Matches = matches;
11 | }
12 |
13 | public override void Sort(JsonNode root)
14 | {
15 | var selected = Finder.FindNodes(root);
16 | foreach (var (name, node) in selected)
17 | {
18 | if (Matches == null || Matches.Matches(name, node))
19 | SortSelected(node);
20 | }
21 | }
22 |
23 | public abstract void SortSelected(JsonNode token);
24 | }
25 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/KeyMover.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class KeyMover
4 | {
5 | private readonly INodeFinder Include;
6 | private readonly NameNodeMatcher[] Destination;
7 | public KeyMover(YamlMappingNode node)
8 | {
9 | Include = new ForwardNodeFinder(node.Go("from").ToList(x => NodeMatcher.Create(x)));
10 | Destination = node.Go("to").ToList(x => new NameNodeMatcher(x.String())).ToArray();
11 | }
12 |
13 | public void MoveKeys(JsonObject obj)
14 | {
15 | var moving = Include.FindNodes(obj).ToList();
16 | var destination = NameNodeMatcher.CreatePath(Destination, obj);
17 | foreach (var (name, node) in moving)
18 | {
19 | node.Parent.AsObject().Remove(name);
20 | destination.Add(name, node);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Abstract/Version.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class Version
4 | {
5 | public DateTime ReleaseTime { get; protected set; }
6 | public string Name { get; protected set; }
7 |
8 | public override string ToString()
9 | {
10 | return $"{this.GetType().Name} {Name}, released {ReleaseTime}";
11 | }
12 |
13 | public abstract void ExtractData(string folder, AppConfig config);
14 | }
15 |
16 | public class GitVersion : Version
17 | {
18 | public readonly GitCommit Commit;
19 | public GitVersion(GitCommit commit)
20 | {
21 | Commit = commit;
22 | ReleaseTime = Commit.CommitTime;
23 | Name = Commit.Message;
24 | }
25 |
26 | public override void ExtractData(string folder, AppConfig config)
27 | {
28 | throw new InvalidOperationException();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System;
2 | global using System.Collections.Generic;
3 | global using System.Collections.ObjectModel;
4 | global using System.Linq;
5 | global using System.Diagnostics;
6 | global using System.Diagnostics.CodeAnalysis;
7 | global using System.IO;
8 | global using System.IO.Compression;
9 | global using System.Net;
10 | global using System.Net.Http;
11 | global using System.Text;
12 | global using System.Text.RegularExpressions;
13 | global using System.Globalization;
14 |
15 | global using Microsoft.VisualBasic.FileIO;
16 | global using YamlDotNet.RepresentationModel;
17 | global using System.Text.Json;
18 | global using System.Text.Json.Nodes;
19 | global using System.Text.Json.Serialization;
20 | global using fNbt;
21 |
22 | global using TryashtarUtils.Utility;
23 | global using TryashtarUtils.Nbt;
24 |
25 | global using SearchOption = System.IO.SearchOption;
26 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/MinecraftVersionHistory.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | 10.0
7 | enable
8 | MinecraftVersionHistory
9 | MinecraftVersionHistory
10 | icon.ico
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Graph/ReleaseBranch.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class ReleaseBranch
4 | {
5 | private readonly List VersionList;
6 | public ReadOnlyCollection Versions => VersionList.AsReadOnly();
7 | public readonly string Name;
8 | public ReleaseBranch(VersionFacts facts, string name, IEnumerable versions)
9 | {
10 | Name = name;
11 | VersionList = versions.Select(x => new VersionNode(x, name)).OrderBy(x => x.Version, facts).ToList();
12 | for (int i = 0; i < VersionList.Count - 1; i++)
13 | {
14 | if (facts.Compare(VersionList[i].Version, VersionList[i + 1].Version) == 0)
15 | throw new ArgumentException($"Can't disambiguate order of {VersionList[i].Version} and {VersionList[i + 1].Version}");
16 | }
17 | for (int i = VersionList.Count - 1; i >= 1; i--)
18 | {
19 | VersionList[i].SetParent(VersionList[i - 1]);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/Profiler.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public static class Profiler
4 | {
5 | private static readonly Stack<(Stopwatch watch, string name)> TimerStack = new();
6 | public static void Start(string name)
7 | {
8 | Console.ForegroundColor = ConsoleColor.Green;
9 | Console.WriteLine($"{new String(' ', TimerStack.Count * 2)}@ {name}");
10 | Console.ResetColor();
11 | var timer = new Stopwatch();
12 | timer.Start();
13 | TimerStack.Push((timer, name));
14 | }
15 |
16 | public static void Stop()
17 | {
18 | var (timer, name) = TimerStack.Pop();
19 | timer.Stop();
20 | Console.ForegroundColor = ConsoleColor.Yellow;
21 | Console.WriteLine($"{new String(' ', TimerStack.Count * 2)}@ {name}: {timer.Elapsed}");
22 | Console.ResetColor();
23 | }
24 |
25 | public static void Run(string name, Action action)
26 | {
27 | Start(name);
28 | action();
29 | Stop();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/BedrockUpdater.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class BedrockUpdater : Updater
4 | {
5 | public BedrockUpdater(AppConfig config) : base(config)
6 | {
7 | }
8 |
9 | protected override VersionConfig VersionConfig => Config.Bedrock;
10 |
11 | protected override IEnumerable FindVersions()
12 | {
13 | foreach (var folder in VersionConfig.InputFolders.Where(x => Directory.Exists(x.Folder)))
14 | {
15 | foreach (var zip in Directory.EnumerateFiles(folder.Folder, "*",
16 | folder.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
17 | {
18 | string ext = Path.GetExtension(zip);
19 | if (ext == ".zip")
20 | yield return new BedrockVersion(zip, VersionConfig.VersionFacts, true);
21 | else if (ext == ".appx")
22 | yield return new BedrockVersion(zip, VersionConfig.VersionFacts, false);
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET Release
2 |
3 | on: push
4 |
5 | jobs:
6 | release:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | with:
11 | submodules: recursive
12 | - name: Setup .NET
13 | uses: actions/setup-dotnet@v3
14 | with:
15 | dotnet-version: 7.0.x
16 | - name: Restore dependencies
17 | run: dotnet restore
18 | - name: Build (Windows)
19 | run: dotnet publish "MinecraftVersionHistory" --runtime win-x64 -p:PublishSingleFile=true /p:DebugType=None /p:DebugSymbols=false --configuration Release --self-contained false
20 | - name: Build (Linux)
21 | run: dotnet publish "MinecraftVersionHistory" --runtime linux-x64 -p:PublishSingleFile=true /p:DebugType=None /p:DebugSymbols=false --configuration Release --self-contained false
22 | - name: Create Release
23 | uses: softprops/action-gh-release@v1
24 | if: startsWith(github.ref, 'refs/tags/')
25 | with:
26 | files: |
27 | ./MinecraftVersionHistory/bin/Release/net7.0/win-x64/publish/MinecraftVersionHistory.exe
28 | ./MinecraftVersionHistory/bin/Release/net7.0/linux-x64/publish/MinecraftVersionHistory
29 |
--------------------------------------------------------------------------------
/MCPModernizer/ModernMCP.cs:
--------------------------------------------------------------------------------
1 | using MinecraftVersionHistory;
2 | using System.IO.Compression;
3 |
4 | namespace MCPModernizer;
5 |
6 | public class ModernMCP : MCP
7 | {
8 | public readonly string Series;
9 | public ModernMCP(string mc_version, string series, string tsrg_file, string[] csv_zips)
10 | {
11 | ClientVersion = mc_version;
12 | Series = series;
13 |
14 | using (var reader = File.OpenText(tsrg_file))
15 | MappingsIO.ParseTsrg(LocalMappings.Client, reader);
16 | using (var reader = File.OpenText(tsrg_file))
17 | MappingsIO.ParseTsrg(LocalMappings.Server, reader);
18 | foreach (var csv in csv_zips)
19 | {
20 | using var zip = ZipFile.OpenRead(csv);
21 | StreamReader? read(string path)
22 | {
23 | var entry = zip.GetEntry(path);
24 | if (entry == null)
25 | return null;
26 | return new(entry.Open());
27 | }
28 | ParseCSVs(
29 | classes: read("classes.csv"),
30 | methods: read("methods.csv"),
31 | fields: read("fields.csv")
32 | );
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/SorterRequirements.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class SorterRequirements
4 | {
5 | public DateTime? After { get; init; }
6 | public bool? SeemsGenerated { get; init; }
7 | public SorterRequirements()
8 | { }
9 | public SorterRequirements(YamlMappingNode node)
10 | {
11 | After = node.Go("after").NullableStructParse(x => DateTime.Parse(x.String()));
12 | SeemsGenerated = node.Go("seems_generated").NullableStructParse(x => Boolean.Parse(x.String()));
13 | }
14 |
15 | public bool MetBy(Version version)
16 | {
17 | if (After != null && version.ReleaseTime < After.Value)
18 | return false;
19 | return true;
20 | }
21 |
22 | public bool MetBy(string path)
23 | {
24 | if (SeemsGenerated != null)
25 | {
26 | using var stream = File.OpenText(path);
27 | bool generated = FileSeemsGenerated(stream);
28 | if (SeemsGenerated != generated)
29 | return false;
30 | }
31 | return true;
32 | }
33 |
34 | private bool FileSeemsGenerated(StreamReader stream)
35 | {
36 | string line;
37 | int i = 0;
38 | while ((line = stream.ReadLine()) != null)
39 | {
40 | i++;
41 | int first_comma = line.IndexOf(',');
42 | int last_comma = line.LastIndexOf(',');
43 | if (first_comma != last_comma)
44 | return false;
45 | }
46 | return true;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Program.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public static class Program
4 | {
5 | public static void Main(string[] args)
6 | {
7 | #if !DEBUG
8 | start:
9 | try
10 | #endif
11 | {
12 | string config_path = Path.Combine(Directory.GetCurrentDirectory(), "config.yaml");
13 | if (args.Length > 0)
14 | config_path = Path.Combine(Directory.GetCurrentDirectory(), args[0]);
15 | var config_file = (YamlMappingNode)YamlHelper.ParseFile(config_path);
16 | var config = new AppConfig(Path.GetDirectoryName(config_path), config_file);
17 |
18 |
19 | var java = new JavaUpdater(config);
20 | #if !DEBUG
21 | try
22 | #endif
23 | {
24 | java.DownloadMissing(config.Java.InputFolders[0].Folder, config);
25 | }
26 | #if !DEBUG
27 | catch (Exception ex)
28 | {
29 | Console.WriteLine("Java version downloader failed!");
30 | Console.WriteLine(ex.ToString());
31 | }
32 | #endif
33 | java.Perform();
34 |
35 | var bedrock = new BedrockUpdater(config);
36 | bedrock.Perform();
37 |
38 | Console.WriteLine("All done!");
39 | }
40 | #if !DEBUG
41 | catch (Exception ex)
42 | {
43 | Console.WriteLine(ex.ToString());
44 | Console.WriteLine();
45 | Console.WriteLine("Press enter to try again");
46 | Console.ReadLine();
47 | goto start;
48 | }
49 | #endif
50 | }
51 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Graph/VersionNode.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class VersionNode
4 | {
5 | public readonly Version Version;
6 | public readonly string ReleaseName;
7 | public VersionNode Parent { get; private set; }
8 | public ReadOnlyCollection Children => ChildNodes.AsReadOnly();
9 |
10 | private readonly List ChildNodes = new();
11 | public VersionNode(Version version, string release)
12 | {
13 | Version = version;
14 | ReleaseName = release;
15 | }
16 |
17 | public void SetParent(VersionNode other)
18 | {
19 | if (Parent != null)
20 | Parent.ChildNodes.Remove(this);
21 | Parent = other;
22 | if (Parent != null)
23 | Parent.ChildNodes.Add(this);
24 | }
25 |
26 | public void AddChild(VersionNode other)
27 | {
28 | other.SetParent(this);
29 | }
30 |
31 | public IEnumerable ToStringRecursive() => ToStringRecursive("");
32 |
33 | private IEnumerable ToStringRecursive(string prefix)
34 | {
35 | string pointer = ChildNodes.Any() ? "│" : "└";
36 | yield return $"{prefix} {pointer} {Version} ({ReleaseName})";
37 | var paths = new List>();
38 | foreach (VersionNode item in this.OrderedChildren())
39 | {
40 | var rest = item.ToStringRecursive(prefix);
41 | paths.Add(rest.ToList());
42 | }
43 | for (int i = 0; i < paths.Count; i++)
44 | {
45 | string extra = paths.Count > 1 ? String.Concat(Enumerable.Repeat(" │", paths.Count - i - 1)) : "";
46 | foreach (var item in paths[i]) yield return extra + item;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/JsonSorterFactory.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public interface IJsonSorter
4 | {
5 | void Sort(JsonNode root);
6 | }
7 |
8 | public static class JsonSorterFactory
9 | {
10 | public static IJsonSorter Create(YamlNode node)
11 | {
12 | if (node is YamlMappingNode map)
13 | {
14 | INodeFinder finder = null;
15 | var path = node.Go("path");
16 | if (path != null)
17 | finder = new ForwardNodeFinder(path.ToList(NodeMatcher.Create));
18 | var up_path = node.Go("up_path");
19 | if (up_path != null)
20 | finder = new BackwardNodeFinder(up_path.ToList(NodeMatcher.Create));
21 | var select = node.Go("by").NullableParse(x => x.ToList(y => new ForwardNodeFinder(y.ToList(NodeMatcher.Create))));
22 | var pick = node.Go("pick").NullableStructParse(x => StringUtils.ParseUnderscoredEnum((string)x)) ?? KeyOrValue.Auto;
23 | var order = node.Go("order").ToStringList();
24 | bool after = node.Go("after").NullableStructParse(x => Boolean.Parse(x.String())) ?? false;
25 | var matches = node.Go("matches").NullableParse(NodeMatcher.Create);
26 | return new JsonSorter(finder, select, pick, order, after, matches);
27 | }
28 | else if (node is YamlSequenceNode seq)
29 | return new MultiJsonSorter(seq.ToList(JsonSorterFactory.Create));
30 | throw new ArgumentException($"Can't turn {node} into a json sorter");
31 | }
32 | }
33 |
34 | public record FileSorter(string[] Files, IJsonSorter Sorter, SorterRequirements Requirements);
35 |
36 | public enum KeyOrValue
37 | {
38 | Key,
39 | Value,
40 | Auto
41 | }
42 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/SnapshotSpec.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class SnapshotSpec
4 | {
5 | private static readonly Regex SnapshotRegex = new(@"(?\d\d)w(?\d\d)(?.)");
6 | public readonly string Release;
7 | private readonly int Year;
8 | private readonly int FirstWeek;
9 | private readonly int LastWeek;
10 | private readonly bool HasWeeks;
11 | public SnapshotSpec(YamlMappingNode node)
12 | {
13 | Year = int.Parse((string)node["year"]);
14 | Release = (string)node["release"];
15 | if (node.TryGet("weeks") is not YamlSequenceNode weeks)
16 | HasWeeks = false;
17 | else
18 | {
19 | HasWeeks = true;
20 | FirstWeek = int.Parse((string)weeks.First());
21 | LastWeek = int.Parse((string)weeks.Last());
22 | }
23 | }
24 |
25 | public static bool IsSnapshot(Version version, out Snapshot snap)
26 | {
27 | var match = SnapshotRegex.Match(version.Name);
28 | snap = match.Success ? new Snapshot(match) : null;
29 | return match.Success;
30 | }
31 |
32 | public bool Matches(Snapshot snapshot)
33 | {
34 | if (snapshot.Year == this.Year)
35 | {
36 | if (!this.HasWeeks)
37 | return true;
38 | return snapshot.Week >= this.FirstWeek && snapshot.Week <= this.LastWeek;
39 | }
40 | return false;
41 | }
42 | }
43 |
44 | public class Snapshot
45 | {
46 | public readonly int Year;
47 | public readonly int Week;
48 | public readonly char Subversion;
49 | public Snapshot(Match match)
50 | {
51 | Year = int.Parse(match.Groups["year"].Value) + 2000;
52 | Week = int.Parse(match.Groups["week"].Value);
53 | Subversion = match.Groups["sub"].Value.Single();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Abstract/VersionConfig.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class VersionConfig
4 | {
5 | public readonly List InputFolders;
6 | public readonly GitRepo GitRepo;
7 | public readonly VersionFacts VersionFacts;
8 | public readonly List NbtTranslations;
9 | public VersionConfig(string folder, AppConfig parent, YamlMappingNode yaml)
10 | {
11 | InputFolders = yaml.Go("version folders").ToList(x => ParseFolder(folder, x));
12 | GitRepo = new GitRepo(Util.FilePath(folder, yaml["repo"]), parent.GitInstallationPath);
13 | VersionFacts = CreateVersionFacts(yaml["version facts"] as YamlMappingNode);
14 | NbtTranslations = yaml.Go("nbt translations").ToList(x => new NbtTranslationOptions((YamlMappingNode)x)) ?? new List();
15 | }
16 |
17 | protected abstract VersionFacts CreateVersionFacts(YamlMappingNode yaml);
18 |
19 | public void TranslateNbt(string path)
20 | {
21 | foreach (var mode in NbtTranslations)
22 | {
23 | if (mode.ShouldTranslate(path))
24 | {
25 | try
26 | {
27 | mode.Translate(path);
28 | }
29 | catch (InvalidDataException ex)
30 | {
31 | Console.WriteLine($"Bad NBT file {path}");
32 | Console.WriteLine(ex.ToString());
33 | }
34 | }
35 | }
36 | }
37 |
38 | private static FolderSpec ParseFolder(string base_folder, YamlNode node)
39 | {
40 | if (node is YamlScalarNode s)
41 | return new(Util.FilePath(base_folder, s), false);
42 | return new FolderSpec(Util.FilePath(base_folder, node.Go("folder")), node.Go("recursive").Bool() ?? false);
43 | }
44 | }
45 |
46 | public record FolderSpec(string Folder, bool Recursive);
47 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/Mappings.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class Mappings
4 | {
5 | private readonly Dictionary Classes = new();
6 | public IEnumerable ClassList => Classes.Values;
7 |
8 | public MappedClass GetClass(string from, Equivalencies eq)
9 | {
10 | foreach (var item in eq.GetEquivalentClasses(from))
11 | {
12 | if (Classes.TryGetValue(item, out var existing))
13 | return existing;
14 | }
15 | return null;
16 | }
17 |
18 | public MappedClass GetOrAddClass(string from)
19 | {
20 | if (Classes.TryGetValue(from, out var existing))
21 | return existing;
22 | return AddClass(from, from);
23 | }
24 |
25 | public MappedClass AddClass(string from, string to)
26 | {
27 | if (from == null || to == null)
28 | throw new NullReferenceException();
29 | MappedClass newclass;
30 | if (Classes.TryGetValue(from, out var existing))
31 | {
32 | #if DEBUG
33 | if (to != existing.NewName)
34 | Console.WriteLine($"Changing {from} from {existing.NewName} to {to}");
35 | #endif
36 | newclass = existing.CopyWith(from, to);
37 | }
38 | else
39 | newclass = new MappedClass(from, to);
40 | Classes[from] = newclass;
41 | return newclass;
42 | }
43 |
44 | public Mappings Reversed()
45 | {
46 | var mappings = new Mappings();
47 | foreach (var c in Classes.Values)
48 | {
49 | var n = mappings.AddClass(c.NewName, c.OldName);
50 | foreach (var f in c.FieldList)
51 | {
52 | n.AddField(f.NewName, f.OldName);
53 | }
54 | foreach (var m in c.MethodList)
55 | {
56 | n.AddMethod(m.NewName, m.OldName, m.Signature);
57 | }
58 | }
59 | return mappings;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/FlatMap.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public record Rename(string OldName, string NewName);
4 | public class FlatMap
5 | {
6 | private readonly Dictionary Classes = new();
7 | private readonly Dictionary Fields = new();
8 | private readonly Dictionary Methods = new();
9 | public IEnumerable ClassMap => Classes.Select(FromKeyValuePair);
10 | public IEnumerable FieldMap => Fields.Select(FromKeyValuePair);
11 | public IEnumerable MethodMap => Methods.Select(FromKeyValuePair);
12 |
13 | private Rename FromKeyValuePair(KeyValuePair pair)
14 | {
15 | return new Rename(pair.Key, pair.Value);
16 | }
17 |
18 | private void Add(Dictionary dict, string from, string to)
19 | {
20 | if (from != to)
21 | {
22 | #if DEBUG
23 | if (dict.TryGetValue(from, out string existing) && existing != to)
24 | Console.WriteLine($"Remapping {from} from {existing} to {to}");
25 | #endif
26 | dict[from] = to;
27 | }
28 | }
29 |
30 | public void AddClass(string from, string to)
31 | {
32 | Add(Classes, from, to);
33 | }
34 |
35 | public void AddField(string from, string to)
36 | {
37 | Add(Fields, from, to);
38 | }
39 |
40 | public void AddMethod(string from, string to)
41 | {
42 | Add(Methods, from, to);
43 | }
44 |
45 | private string Get(Dictionary dict, string name)
46 | {
47 | if (dict.TryGetValue(name, out string existing))
48 | return existing;
49 | return null;
50 | }
51 |
52 | public string GetClass(string from)
53 | {
54 | return Get(Classes, from);
55 | }
56 |
57 | public string GetField(string from)
58 | {
59 | return Get(Fields, from);
60 | }
61 |
62 | public string GetMethod(string from)
63 | {
64 | return Get(Methods, from);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/CommandRunner.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public static class CommandRunner
4 | {
5 | public static ProcessResult RunCommand(string directory, string exe, string args, TextWriter output, TextWriter error)
6 | {
7 | #if DEBUG
8 | Console.ForegroundColor = ConsoleColor.Magenta;
9 | Console.WriteLine($"Running this command: {exe} {args}");
10 | #endif
11 | Console.ForegroundColor = ConsoleColor.DarkGray;
12 | var result = new ProcessWrapper(directory, exe, args, output, error).Result;
13 | Console.ResetColor();
14 | return result;
15 | }
16 |
17 | public static ProcessResult RunCommand(string directory, string exe, string args)
18 | {
19 | return RunCommand(directory, exe, args, Console.Out, Console.Out);
20 | }
21 |
22 | public static ProcessResult RunJavaCommand(string cd, IEnumerable java, string input)
23 | {
24 | return RunJavaCommands(cd, java.Select(x => (x, input)));
25 | }
26 |
27 | public static ProcessResult RunJavaCombos(string cd, IEnumerable java, IEnumerable inputs)
28 | {
29 | var combinations = from x in java from y in inputs select (x, y);
30 | return RunJavaCommands(cd, combinations);
31 | }
32 |
33 | public static ProcessResult RunJavaCommands(string cd, IEnumerable<(string java, string input)> commands)
34 | {
35 | ProcessResult result = default;
36 | #if DEBUG
37 | int i = 0;
38 | #endif
39 | foreach (var (java, input) in commands)
40 | {
41 | #if DEBUG
42 | i++;
43 | Console.ForegroundColor = ConsoleColor.Magenta;
44 | Console.WriteLine($"Attempt #{i} of {commands.Count()}");
45 | Console.WriteLine($"Install: {java}");
46 | Console.WriteLine($"Input: {input}");
47 | #endif
48 | result = RunCommand(cd, java, input);
49 | if (result.ExitCode == 0)
50 | return result;
51 | }
52 |
53 | return result;
54 | }
55 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/ProcessWrapper.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace MinecraftVersionHistory;
5 |
6 | public record ProcessResult(int ExitCode, string Output, string Error);
7 | public class ProcessWrapper
8 | {
9 | public ProcessResult Result => Pending.Result;
10 | private readonly Task Pending;
11 | private readonly TextWriter Output;
12 | private readonly TextWriter Error;
13 | public ProcessWrapper(string directory, string filename, string arguments, TextWriter output, TextWriter error)
14 | {
15 | Output = output;
16 | Error = error;
17 | var process = new Process()
18 | {
19 | StartInfo = new ProcessStartInfo()
20 | {
21 | FileName = filename,
22 | WorkingDirectory = directory,
23 | Arguments = arguments,
24 | UseShellExecute = false,
25 | CreateNoWindow = true,
26 | RedirectStandardOutput = true,
27 | RedirectStandardError = true
28 | }
29 | };
30 | Pending = StartProcess(process);
31 | }
32 | public ProcessWrapper(string directory, string filename, string arguments) : this(directory, filename, arguments, Console.Out, Console.Out)
33 | { }
34 |
35 | private async Task StartProcess(Process process)
36 | {
37 | var output = new StringBuilder();
38 | var error = new StringBuilder();
39 | process.OutputDataReceived += (sender, e) =>
40 | {
41 | if (e.Data != null)
42 | {
43 | output.AppendLine(e.Data);
44 | if (Output != null)
45 | Output.WriteLine(e.Data);
46 | }
47 | };
48 | process.ErrorDataReceived += (sender, e) =>
49 | {
50 | if (e.Data != null)
51 | {
52 | error.AppendLine(e.Data);
53 | if (Error != null)
54 | Error.WriteLine(e.Data);
55 | }
56 | };
57 | process.Start();
58 | process.BeginOutputReadLine();
59 | process.BeginErrorReadLine();
60 | await process.WaitForExitAsync();
61 | return new ProcessResult(process.ExitCode, output.ToString(), error.ToString());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/NodeFinders.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public interface INodeFinder
4 | {
5 | IEnumerable<(string name, JsonNode node)> FindNodes(JsonNode start);
6 | }
7 |
8 | public class ForwardNodeFinder : INodeFinder
9 | {
10 | private readonly NodeMatcher[] Path;
11 | public ForwardNodeFinder(IEnumerable path)
12 | {
13 | Path = path.ToArray();
14 | }
15 |
16 | public IEnumerable<(string name, JsonNode node)> FindNodes(JsonNode start)
17 | {
18 | IEnumerable<(string name, JsonNode node)> selected = new[] { ((string)null, start) };
19 | foreach (var matcher in Path)
20 | {
21 | selected = matcher.Follow(selected.Select(x => x.node));
22 | }
23 | return selected;
24 | }
25 | }
26 |
27 | public class BackwardNodeFinder : INodeFinder
28 | {
29 | private readonly int Count;
30 | private readonly ForwardNodeFinder Forward;
31 | public BackwardNodeFinder(IEnumerable path)
32 | {
33 | Count = path.Count();
34 | Forward = new(path.Reverse());
35 | }
36 |
37 | public IEnumerable<(string name, JsonNode node)> FindNodes(JsonNode start)
38 | {
39 | foreach (var item in AllNodes(null, start))
40 | {
41 | if (MatchesPath(item.node))
42 | yield return item;
43 | }
44 | }
45 |
46 | private bool MatchesPath(JsonNode node)
47 | {
48 | if (Count == 0)
49 | return true;
50 | var parent = node;
51 | for (int i = 0; i < Count; i++)
52 | {
53 | parent = parent.Parent;
54 | if (parent == null)
55 | return false;
56 | }
57 | return Forward.FindNodes(parent).ToList().Any(x => x.node == node);
58 | }
59 |
60 | private IEnumerable<(string name, JsonNode node)> AllNodes(string name, JsonNode root)
61 | {
62 | yield return (name, root);
63 | if (root is JsonObject obj)
64 | {
65 | foreach (var item in obj)
66 | {
67 | foreach (var sub in AllNodes(item.Key, item.Value))
68 | {
69 | yield return sub;
70 | }
71 | }
72 | }
73 | else if (root is JsonArray arr)
74 | {
75 | foreach (var item in arr)
76 | {
77 | foreach (var sub in AllNodes(null, item))
78 | {
79 | yield return sub;
80 | }
81 | }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/BedrockVersion.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class BedrockVersion : Version
4 | {
5 | public readonly string AppxPath;
6 |
7 | public BedrockVersion(string path, VersionFacts facts, bool unzip)
8 | {
9 | if (unzip)
10 | {
11 | Console.WriteLine($"Extracting APPX for {Path.GetFileName(path)}...");
12 | using (ZipArchive zip = ZipFile.OpenRead(path))
13 | {
14 | var appx = GetMainAppx(zip);
15 | AppxPath = Path.ChangeExtension(path, ".appx");
16 | appx.ExtractToFile(AppxPath);
17 | }
18 | File.Delete(path);
19 | }
20 | else
21 | AppxPath = path;
22 |
23 | using ZipArchive zip2 = ZipFile.OpenRead(AppxPath);
24 | Name = facts.CustomName(Path.GetFileNameWithoutExtension(path));
25 | if (Name == null)
26 | {
27 | var manifest = zip2.GetEntry("AppxManifest.xml");
28 | using var read = new StreamReader(manifest.Open());
29 | string data = read.ReadToEnd();
30 | // too lazy to parse xml
31 | int version_index = data.IndexOf("Version=\"") + "Version=\"".Length;
32 | Name = data[version_index..data.IndexOf("\"", version_index)];
33 | }
34 |
35 | ReleaseTime = zip2.Entries[0].LastWriteTime.UtcDateTime;
36 | }
37 |
38 | private ZipArchiveEntry GetMainAppx(ZipArchive zip)
39 | {
40 | foreach (var entry in zip.Entries)
41 | {
42 | string filename = Path.GetFileName(entry.FullName);
43 | // example: Minecraft.Windows_1.1.0.0_x64_UAP.Release.appx
44 | if (filename.StartsWith("Minecraft.Windows") && Path.GetExtension(filename) == ".appx")
45 | return entry;
46 | }
47 |
48 | throw new FileNotFoundException($"Could not find main APPX");
49 | }
50 |
51 | public override void ExtractData(string folder, AppConfig config)
52 | {
53 | var bedrock_config = config.Bedrock;
54 | using (ZipArchive zip = ZipFile.OpenRead(AppxPath))
55 | {
56 | foreach (var entry in zip.Entries)
57 | {
58 | if (entry.FullName.StartsWith("data/") && Path.GetExtension(entry.FullName) != ".zip")
59 | {
60 | Directory.CreateDirectory(Path.Combine(folder, Path.GetDirectoryName(entry.FullName)));
61 | entry.ExtractToFile(Path.Combine(folder, entry.FullName));
62 | }
63 | }
64 | }
65 |
66 | foreach (var merger in bedrock_config.PackMergers)
67 | {
68 | merger.Merge(folder);
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/JavaUpdater.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class JavaUpdater : Updater
4 | {
5 | public JavaUpdater(AppConfig config) : base(config)
6 | {
7 | }
8 |
9 | protected override VersionConfig VersionConfig => Config.Java;
10 |
11 | protected override IEnumerable FindVersions()
12 | {
13 | foreach (var folder in VersionConfig.InputFolders.Where(x => Directory.Exists(x.Folder)))
14 | {
15 | foreach (var version in Directory.EnumerateDirectories(folder.Folder, "*",
16 | folder.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
17 | {
18 | if (JavaVersion.LooksValid(version))
19 | yield return new JavaVersion(version, VersionConfig.VersionFacts);
20 | }
21 | }
22 | }
23 |
24 | private const string LAUNCHER_MANIFEST = "https://piston-meta.mojang.com/mc/game/version_manifest.json";
25 |
26 | public void DownloadMissing(string destination_folder, AppConfig config)
27 | {
28 | Profiler.Start("Checking for new versions");
29 | var web_versions = (JsonArray)JsonNode.Parse(Util.DownloadString(LAUNCHER_MANIFEST))["versions"];
30 | var commits = config.Java.GitRepo.CommittedVersions().Select(x => x.Message).ToHashSet();
31 | var local_versions = FindVersions().ToDictionary(x => x.Name);
32 | bool found_any = false;
33 | foreach (var version in web_versions)
34 | {
35 | var name = (string)version["id"];
36 | var url = (string)version["url"];
37 | if (commits.Contains(name))
38 | continue;
39 | if (local_versions.TryGetValue(name, out var existing) && File.Exists(existing.JarFilePath) &&
40 | File.Exists(existing.LauncherJsonPath))
41 | continue;
42 | found_any = true;
43 | var download_location = Path.Combine(destination_folder, name);
44 | var json_location = Path.Combine(download_location, name + ".json");
45 | var jar_location = Path.Combine(download_location, name + ".jar");
46 | Console.WriteLine($"Downloading new version: {name}");
47 | Directory.CreateDirectory(download_location);
48 | if (!File.Exists(json_location))
49 | Util.DownloadFile(url, json_location);
50 | var client_jar =
51 | (string)JsonObject.Parse(File.ReadAllText(json_location))["downloads"]["client"]["url"];
52 | if (!File.Exists(jar_location))
53 | Util.DownloadFile(client_jar, jar_location);
54 | }
55 |
56 | Profiler.Stop();
57 | if (found_any)
58 | CreateGraph();
59 | }
60 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/MappedClass.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public record MappedField(string OldName, string NewName);
4 | public record MappedMethod(string OldName, string NewName, string Signature);
5 | public class MappedClass
6 | {
7 | public readonly string OldName;
8 | public readonly string NewName;
9 | private readonly Dictionary<(string name, string signature), MappedMethod> Methods = new();
10 | private readonly Dictionary Fields = new();
11 | public MappedClass(string oldname, string newname)
12 | {
13 | OldName = oldname;
14 | NewName = newname;
15 | }
16 |
17 | public IEnumerable MethodList => Methods.Values;
18 | public IEnumerable FieldList => Fields.Values;
19 |
20 | public MappedClass CopyWith(string oldname, string newname)
21 | {
22 | var copy = new MappedClass(oldname, newname);
23 | foreach (var item in MethodList)
24 | {
25 | copy.AddMethod(item.OldName, item.NewName, item.Signature);
26 | }
27 | foreach (var item in FieldList)
28 | {
29 | copy.AddField(item.OldName, item.NewName);
30 | }
31 | return copy;
32 | }
33 |
34 | public MappedMethod AddMethod(string from, string to, string signature)
35 | {
36 | if (from == null || to == null)
37 | throw new NullReferenceException();
38 | #if DEBUG
39 | if (Methods.TryGetValue((from, signature), out var existing))
40 | {
41 | if (to != existing.NewName)
42 | Console.WriteLine($"Changing {from} from {existing.NewName} to {to}");
43 | }
44 | #endif
45 | var method = new MappedMethod(from, to, signature);
46 | Methods[(from, signature)] = method;
47 | return method;
48 | }
49 |
50 | public MappedField AddField(string from, string to)
51 | {
52 | if (from == null || to == null)
53 | throw new NullReferenceException();
54 | #if DEBUG
55 | if (Fields.TryGetValue(from, out var existing))
56 | {
57 | if (to != existing.NewName)
58 | Console.WriteLine($"Changing {from} from {existing.NewName} to {to}");
59 | }
60 | #endif
61 | var field = new MappedField(from, to);
62 | Fields[from] = field;
63 | return field;
64 | }
65 |
66 | public MappedMethod GetMethod(string from, string signature, Equivalencies eq)
67 | {
68 | foreach (var item in eq.GetEquivalentMethods(from))
69 | {
70 | if (Methods.TryGetValue((item, signature), out var existing))
71 | return existing;
72 | }
73 | return null;
74 | }
75 |
76 | public MappedField GetField(string from, Equivalencies eq)
77 | {
78 | foreach (var item in eq.GetEquivalentFields(from))
79 | {
80 | if (Fields.TryGetValue(item, out var existing))
81 | return existing;
82 | }
83 | return null;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/GitRepo.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class GitRepo
4 | {
5 | public readonly string Folder;
6 | private readonly string GitInstall;
7 |
8 | public GitRepo(string repo_folder, string git_install)
9 | {
10 | Folder = repo_folder;
11 | GitInstall = git_install;
12 | }
13 |
14 | public ProcessResult Run(string args, TextWriter output, TextWriter error)
15 | {
16 | return CommandRunner.RunCommand(Folder, GitInstall, args, output, error);
17 | }
18 |
19 | public ProcessResult Run(string args)
20 | {
21 | return Run(args, Console.Out, Console.Out);
22 | }
23 |
24 | public void Init()
25 | {
26 | Run("init");
27 | }
28 |
29 | public void Commit(string message, DateTime? date = null)
30 | {
31 | Run("add -A");
32 | if (date == null)
33 | Run($"commit -m \"{message}\"");
34 | else
35 | {
36 | Environment.SetEnvironmentVariable("GIT_COMMITTER_DATE", date.ToString());
37 | Run($"commit --date=\"{date}\" -m \"{message}\"");
38 | }
39 | }
40 |
41 | public void CheckoutBranch(string branch, string? hash = null)
42 | {
43 | if (hash == null)
44 | Run($"checkout -b \"{branch}\"");
45 | else
46 | Run($"checkout -b \"{branch}\" {hash}");
47 | }
48 |
49 | public void Checkout(string branch)
50 | {
51 | Run($"checkout \"{branch}\"");
52 | }
53 |
54 | public void MakeBranch(string branch, string? hash = null)
55 | {
56 | if (hash == null)
57 | Run($"branch \"{branch}\"");
58 | else
59 | Run($"branch \"{branch}\" {hash}");
60 | }
61 |
62 | public void DeleteBranch(string branch)
63 | {
64 | Run($"branch -D \"{branch}\"");
65 | }
66 |
67 | public string BranchHash(string branch)
68 | {
69 | return Run($"rev-parse \"{branch}\"", null, Console.Out).Output[..40];
70 | }
71 |
72 | public void Rebase(string from, string to)
73 | {
74 | Run($"rebase \"{from}\" \"{to}\" -X theirs");
75 | }
76 |
77 | public IEnumerable CommittedVersions()
78 | {
79 | string[] all = StringUtils.SplitLines(Run(
80 | "log --exclude=\"refs/notes/*\" --all --pretty=\"%H___%s___%ad___%p\" --date=format:\"%Y/%m/%d\"",
81 | null, Console.Out).Output).ToArray();
82 | foreach (var item in all)
83 | {
84 | if (String.IsNullOrEmpty(item))
85 | continue;
86 | var entries = item.Split("___");
87 | yield return new GitCommit(entries[0], entries[1], DateTime.Parse(entries[2]));
88 | }
89 | }
90 | }
91 |
92 | public class GitCommit
93 | {
94 | public readonly string Hash;
95 | public readonly string Message;
96 | public readonly DateTime CommitTime;
97 |
98 | public GitCommit(string hash, string message, DateTime time)
99 | {
100 | Hash = hash;
101 | Message = message;
102 | CommitTime = time;
103 | }
104 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31912.275
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftVersionHistory", "MinecraftVersionHistory\MinecraftVersionHistory.csproj", "{72B2765F-9241-4D00-9D40-98E006FE9B92}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fNbt", "fNbt\fNbt\fNbt.csproj", "{377B21B7-860C-4994-B48B-2069DB5E0C80}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TryashtarUtils.Nbt", "utils.nbt\TryashtarUtils.Nbt.csproj", "{39D5A421-C3CE-4D15-A782-EADC30ABE57E}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TryashtarUtils.Utility", "utils.utility\TryashtarUtils.Utility.csproj", "{8A7342FF-1239-495B-82C7-4ACAB00FE66F}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPModernizer", "MCPModernizer\MCPModernizer.csproj", "{F674F77D-D5E5-43C2-98C5-86AF4E57C69D}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {72B2765F-9241-4D00-9D40-98E006FE9B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {72B2765F-9241-4D00-9D40-98E006FE9B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {72B2765F-9241-4D00-9D40-98E006FE9B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {72B2765F-9241-4D00-9D40-98E006FE9B92}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {377B21B7-860C-4994-B48B-2069DB5E0C80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {377B21B7-860C-4994-B48B-2069DB5E0C80}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {377B21B7-860C-4994-B48B-2069DB5E0C80}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {377B21B7-860C-4994-B48B-2069DB5E0C80}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {39D5A421-C3CE-4D15-A782-EADC30ABE57E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {39D5A421-C3CE-4D15-A782-EADC30ABE57E}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {39D5A421-C3CE-4D15-A782-EADC30ABE57E}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {39D5A421-C3CE-4D15-A782-EADC30ABE57E}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {8A7342FF-1239-495B-82C7-4ACAB00FE66F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {8A7342FF-1239-495B-82C7-4ACAB00FE66F}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {8A7342FF-1239-495B-82C7-4ACAB00FE66F}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {8A7342FF-1239-495B-82C7-4ACAB00FE66F}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {F674F77D-D5E5-43C2-98C5-86AF4E57C69D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {F674F77D-D5E5-43C2-98C5-86AF4E57C69D}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {F674F77D-D5E5-43C2-98C5-86AF4E57C69D}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {F674F77D-D5E5-43C2-98C5-86AF4E57C69D}.Release|Any CPU.Build.0 = Release|Any CPU
42 | EndGlobalSection
43 | GlobalSection(SolutionProperties) = preSolution
44 | HideSolutionNode = FALSE
45 | EndGlobalSection
46 | GlobalSection(ExtensibilityGlobals) = postSolution
47 | SolutionGuid = {479D2921-1F3C-42EF-9984-47E4CF76A1A5}
48 | EndGlobalSection
49 | EndGlobal
50 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Graph/VersionGraph.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class VersionGraph
4 | {
5 | private readonly VersionNode Root;
6 | private readonly List Branches = new();
7 | private readonly VersionFacts Facts;
8 | public VersionGraph(VersionFacts facts, IEnumerable versions)
9 | {
10 | Facts = facts;
11 | versions = versions.Where(x => !facts.ShouldSkip(x));
12 | var releases = versions.GroupBy(x => Facts.GetReleaseName(x));
13 | foreach (var branch in releases)
14 | {
15 | Branches.Add(new ReleaseBranch(facts, branch.Key, branch));
16 | }
17 | var sorter = new BranchSorter(facts);
18 | Branches.Sort(sorter);
19 | Root = Branches.First().Versions.First();
20 | for (int i = Branches.Count - 1; i >= 1; i--)
21 | {
22 | // set cross-branch parents with educated guesses
23 | // pick the last version in the previous branch that's older than the first version in this branch
24 | // skip "insane" branches (like april fools versions)
25 | var start = Branches[i].Versions.First();
26 | var sane_parent = Branches.Take(i).Last(x => !Facts.IsInsaneRelease(x.Name)).Versions
27 | .Last(x => !Facts.IsInsaneVersion(x.Version) && Facts.Compare(start.Version, x.Version) > 0);
28 | start.SetParent(sane_parent);
29 | }
30 | foreach (var version in versions)
31 | {
32 | string special = Facts.SpecialParent(version);
33 | if (special != null)
34 | {
35 | var found = versions.FirstOrDefault(x => x.Name == special);
36 | if (found != null)
37 | {
38 | var node1 = FindNode(version);
39 | var node2 = FindNode(found);
40 | node1.SetParent(node2);
41 | }
42 | }
43 | }
44 | }
45 |
46 | public IEnumerable Flatten()
47 | {
48 | var stack = new Stack();
49 | stack.Push(Root);
50 | while (stack.Any())
51 | {
52 | var node = stack.Pop();
53 | yield return node;
54 | foreach (var child in node.OrderedChildren().Reverse())
55 | {
56 | stack.Push(child);
57 | }
58 | }
59 | }
60 |
61 | private VersionNode FindNode(Version version)
62 | {
63 | var release = Facts.GetReleaseName(version);
64 | var branch = Branches.First(x => x.Name == release);
65 | return branch.Versions.First(x => x.Version == version);
66 | }
67 |
68 | public override string ToString()
69 | {
70 | return String.Join("\n", Root.ToStringRecursive());
71 | }
72 |
73 | private class BranchSorter : IComparer
74 | {
75 | private readonly VersionFacts Facts;
76 | public BranchSorter(VersionFacts facts)
77 | {
78 | Facts = facts;
79 | }
80 |
81 | public int Compare(ReleaseBranch? x, ReleaseBranch? y)
82 | {
83 | return Facts.Compare(x.Versions.First().Version, y.Versions.First().Version);
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Minecraft Version History
2 | I use this program to generate a git history that shows the changes to Minecraft with every release. As far as I know, only I use it, so the documentation isn't amazing. But if you want to use it, just ask and I'd be happy to help you set it up!
3 |
4 |
5 |
6 | It works for both Java and Bedrock editions. You need to use a `config.yaml` file to determine a lot of functionality. Either put it next to the executable, or pass its path as an argument. You can view [the config I personally use](personal_config.yaml) as a reference.
7 |
8 | **Java Features**
9 | * Versions are read as they appear in the `.minecraft/versions` directory, with a folder containing an identically-named jar and JSON manifest.
10 | * The entire contents of the jar is extracted to the `jar` folder. You can use the config to exclude unnecessary files like `.class` files or `META-INF`, leaving only the vanilla resource/data pack.
11 | - Generated files that change often, such as the `shipwreck_supply` loot table, can be configured to be sorted to avoid clogging diffs.
12 | * All NBT files (mostly structures from the jar) are converted to indented SNBT and stored alongside their binary counterparts.
13 | - You can configure specific keys to be removed from both copies (like `DataVersion`, which is incremented every release).
14 | * For versions that support it, data generators are run and their output stored in the `reports` folder. The server jar is downloaded automatically to accomplish this. This includes the command tree, the block states registry, and the other registries, as well as biome data in more recent versions.
15 | * The source code is decompiled and stored in the `source` folder.
16 | - You can choose between [cfr](https://github.com/leibnitz27/cfr) (my personal choice) or [FernFlower](https://github.com/fesh0r/fernflower).
17 | - For versions that support it, Mojang mappings are automatically downloaded and used to deobfuscate the jar.
18 | - If the server jar can be downloaded, it is decompiled as well, ensuring that all endpoint-specific code is included.
19 |
20 | **Bedrock Features**
21 | * The versions are read as APPX archives.
22 | * The entire contents of the APPX is extracted to the `data` folder.
23 | * NBT files are converted, as above.
24 | * Since Bedrock stores its vanilla resource/behavior packs in slices, you can with configuration merge them into a "final" pack. Whether files across slices should be overwritten or merge in some way is also configurable. These final packs are stored in the `latest_packs` folder.
25 |
26 | **History Order**
27 |
28 | The program organizes the versions by branches that correspond to a release. Currently, the program has no way to determine what release each version belongs to, so it must be configured. Once you do that, however, the program does a decent job at figuring out by itself how to construct the branches.
29 |
30 | Within a branch, all versions are sorted by release date. You can use the config to override this order. The branches themselves are also arranged by release date, where the first version of a branch will parent to the newest version of the previous branch that's still older than the version in question. So far, this seems to accurately recreate the Minecraft version history tree, but you can also use the config to explicitly define parents.
31 |
32 |
33 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/NodeMatchers.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class NodeMatcher
4 | {
5 | public static NodeMatcher Create(YamlNode node)
6 | {
7 | if (node is YamlScalarNode scalar)
8 | {
9 | string val = scalar.Value;
10 | if (val.StartsWith("@"))
11 | return new RegexNodeMatcher(new Regex(val[1..]));
12 | if (val == "*")
13 | return new AlwaysNodeMatcher();
14 | return new NameNodeMatcher(val);
15 | }
16 | if (node is YamlMappingNode map)
17 | return new TemplateNodeMatcher(map);
18 | throw new ArgumentException(nameof(node));
19 | }
20 |
21 | public abstract bool Matches(string name, JsonNode node);
22 |
23 | public IEnumerable<(string name, JsonNode node)> Follow(JsonNode start)
24 | {
25 | if (start is JsonObject obj)
26 | return obj.Where(x => Matches(x.Key, x.Value)).Select(x => (x.Key, x.Value));
27 | else if (start is JsonArray arr)
28 | return arr.Where(x => Matches(null, x)).Select(x => ((string)null, x));
29 | return Array.Empty<(string, JsonNode)>();
30 | }
31 |
32 | public IEnumerable<(string name, JsonNode node)> Follow(IEnumerable starts)
33 | {
34 | foreach (var start in starts)
35 | {
36 | foreach (var item in Follow(start))
37 | yield return item;
38 | }
39 | }
40 | }
41 |
42 | public class NameNodeMatcher : NodeMatcher
43 | {
44 | public readonly string Name;
45 | public NameNodeMatcher(string name)
46 | {
47 | Name = name;
48 | }
49 |
50 | public override bool Matches(string name, JsonNode node)
51 | {
52 | if (name == Name)
53 | return true;
54 | if (node.Parent is JsonArray arr && int.TryParse(Name, out int index) && arr[index] == node)
55 | return true;
56 | return false;
57 | }
58 |
59 | public static JsonObject CreatePath(IEnumerable path, JsonObject start)
60 | {
61 | foreach (var item in path)
62 | {
63 | JsonObject next;
64 | if (start.TryGetPropertyValue(item.Name, out var existing))
65 | next = (JsonObject)existing;
66 | else
67 | {
68 | next = new JsonObject();
69 | start.Add(item.Name, next);
70 | }
71 | start = next;
72 | }
73 | return start;
74 | }
75 | }
76 |
77 | public class RegexNodeMatcher : NodeMatcher
78 | {
79 | public readonly Regex RegExpression;
80 | public RegexNodeMatcher(Regex regex)
81 | {
82 | RegExpression = regex;
83 | }
84 |
85 | public override bool Matches(string name, JsonNode node)
86 | {
87 | if (name == null)
88 | return false;
89 | return RegExpression.IsMatch(name);
90 | }
91 | }
92 |
93 | public class TemplateNodeMatcher : NodeMatcher
94 | {
95 | private readonly YamlMappingNode Template;
96 | public TemplateNodeMatcher(YamlMappingNode template)
97 | {
98 | Template = template;
99 | }
100 |
101 | private static bool ValueEquals(JsonNode json, YamlNode yaml)
102 | {
103 | if (yaml is YamlScalarNode scalar)
104 | return (string)scalar == json.ToString();
105 | throw new ArgumentException();
106 | }
107 |
108 | public override bool Matches(string name, JsonNode node)
109 | {
110 | if (node is not JsonObject obj)
111 | return false;
112 | foreach (var item in Template)
113 | {
114 | if (!(obj.TryGetPropertyValue((string)item.Key, out var value) && ValueEquals(value, item.Value)))
115 | return false;
116 | }
117 | return true;
118 | }
119 | }
120 |
121 | public class AlwaysNodeMatcher : NodeMatcher
122 | {
123 | public override bool Matches(string name, JsonNode node)
124 | {
125 | return true;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/JsonSorter/JsonSorter.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class JsonSorter : PathedJsonSorter
4 | {
5 | private readonly INodeFinder[] SortBy;
6 | private readonly KeyOrValue Pick;
7 | private readonly List Order;
8 | private readonly bool After;
9 | public JsonSorter(INodeFinder finder, IEnumerable sort_by, KeyOrValue pick, IEnumerable order, bool after, NodeMatcher matches) : base(finder, matches)
10 | {
11 | SortBy = sort_by?.ToArray();
12 | Pick = pick;
13 | Order = order?.ToList();
14 | After = after;
15 | }
16 |
17 | public override void SortSelected(JsonNode token)
18 | {
19 | if (token is JsonObject obj)
20 | {
21 | IEnumerable> tokens = new List>(obj);
22 | obj.Clear();
23 | tokens = tokens.OrderBy(x => x, new Comparer(this));
24 | foreach (var item in tokens)
25 | {
26 | obj.Add(item.Key, item.Value);
27 | }
28 | }
29 | else if (token is JsonArray arr)
30 | {
31 | IEnumerable tokens = new List(arr);
32 | arr.Clear();
33 | tokens = tokens.OrderBy(x => x, new Comparer(this));
34 | foreach (var item in tokens)
35 | {
36 | arr.Add(item);
37 | }
38 | }
39 | }
40 |
41 | private string GetSortItem(INodeFinder? find, string name, JsonNode node)
42 | {
43 | if (find != null)
44 | {
45 | var result = find.FindNodes(node);
46 | if (result.Any())
47 | (name, node) = result.First();
48 | }
49 | if (name != null && Pick != KeyOrValue.Value)
50 | return name;
51 | if (node is JsonValue val)
52 | return val.ToString();
53 | return null;
54 | }
55 |
56 | private class Comparer : IComparer>, IComparer
57 | {
58 | private readonly JsonSorter Owner;
59 | public Comparer(JsonSorter owner) { Owner = owner; }
60 |
61 | private int Compare(string xs, string ys)
62 | {
63 | if (Owner.Order == null)
64 | {
65 | if (decimal.TryParse(xs, out decimal xn) && decimal.TryParse(ys, out decimal yn))
66 | return xn.CompareTo(yn);
67 | return String.Compare(xs, ys);
68 | }
69 | int xi = Owner.Order.IndexOf(xs);
70 | int yi = Owner.Order.IndexOf(ys);
71 | if (xi == -1)
72 | xi = Owner.After ? int.MinValue : int.MaxValue;
73 | if (yi == -1)
74 | yi = Owner.After ? int.MinValue : int.MaxValue;
75 | return xi.CompareTo(yi);
76 | }
77 |
78 | public int Compare(KeyValuePair x, KeyValuePair y)
79 | {
80 | if (Owner.SortBy == null)
81 | {
82 | string xs = Owner.GetSortItem(null, x.Key, x.Value);
83 | string ys = Owner.GetSortItem(null, y.Key, y.Value);
84 | return Compare(xs, ys);
85 | }
86 | foreach (var item in Owner.SortBy)
87 | {
88 | string xs = Owner.GetSortItem(item, x.Key, x.Value);
89 | string ys = Owner.GetSortItem(item, y.Key, y.Value);
90 | int result = Compare(xs, ys);
91 | if (result != 0)
92 | return result;
93 | }
94 | return 0;
95 | }
96 |
97 | public int Compare(JsonNode? x, JsonNode? y)
98 | {
99 | if (Owner.SortBy == null)
100 | {
101 | string xs = Owner.GetSortItem(null, null, x);
102 | string ys = Owner.GetSortItem(null, null, y);
103 | return Compare(xs, ys);
104 | }
105 | foreach (var item in Owner.SortBy)
106 | {
107 | string xs = Owner.GetSortItem(item, null, x);
108 | string ys = Owner.GetSortItem(item, null, y);
109 | int result = Compare(xs, ys);
110 | if (result != 0)
111 | return result;
112 | }
113 | return 0;
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/PackMerger.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class PackMerger
4 | {
5 | private readonly string LayersFolder;
6 | private readonly string OutputFolder;
7 | private readonly List Exclude;
8 | private readonly List Layers;
9 | private readonly List MergingSpecs;
10 | public PackMerger(YamlMappingNode node)
11 | {
12 | LayersFolder = (string)node.TryGet("input");
13 | OutputFolder = (string)node.TryGet("output");
14 | Exclude = node.Go("exclude").ToList(x => new Regex((string)x)) ?? new();
15 | Layers = node.Go("layers").ToStringList() ?? new List();
16 | MergingSpecs = node.Go("merging").ToList(x => new MergingSpec((YamlMappingNode)x)) ?? new List();
17 | }
18 |
19 | public void Merge(string root)
20 | {
21 | Profiler.Start("Merging vanilla packs");
22 | string input_folder = Path.Combine(root, LayersFolder);
23 | string output_folder = Path.Combine(root, OutputFolder);
24 | Directory.CreateDirectory(output_folder);
25 | foreach (var layer in Layers)
26 | {
27 | if (layer == "vanilla_*")
28 | {
29 | var vanilla_slice_folders = Directory.GetDirectories(input_folder, "vanilla_*", SearchOption.TopDirectoryOnly);
30 | var vanilla_slices = new List<(int[], string)>();
31 | foreach (var folder in vanilla_slice_folders)
32 | {
33 | var name = Path.GetFileName(folder);
34 | if (!name.StartsWith("vanilla_"))
35 | continue;
36 | name = name.Substring("vanilla_".Length);
37 | var points = name.Split('.');
38 | int[] int_points;
39 | try
40 | {
41 | int_points = points.Select(x => int.Parse(x)).ToArray();
42 | }
43 | catch (FormatException)
44 | {
45 | continue;
46 | }
47 | vanilla_slices.Add((int_points, folder));
48 | }
49 | vanilla_slices.Sort((x, y) => CompareVersions(x.Item1, y.Item1));
50 | foreach (var slice in vanilla_slices)
51 | {
52 | Console.WriteLine($"Applying {Path.GetFileName(slice.Item2)}");
53 | MergeLayer(slice.Item2, output_folder);
54 | }
55 | }
56 | else
57 | {
58 | var pack = Path.Combine(input_folder, layer);
59 | if (!Directory.Exists(pack))
60 | {
61 | Console.WriteLine($"Skipping {layer} (doesn't exist)");
62 | continue;
63 | }
64 |
65 | Console.WriteLine($"Applying {layer}");
66 | MergeLayer(pack, output_folder);
67 | }
68 | }
69 | Profiler.Stop();
70 | }
71 |
72 | private int CompareVersions(int[] v1, int[] v2)
73 | {
74 | foreach (var (p1, p2) in v1.Zip(v2))
75 | {
76 | if (p1 > p2) return 1;
77 | if (p1 < p2) return -1;
78 | }
79 | if (v1.Length > v2.Length) return 1;
80 | if (v2.Length > v1.Length) return -1;
81 | return 0;
82 | }
83 |
84 | private void MergeLayer(string layer_folder, string output_folder)
85 | {
86 | foreach (var file in Directory.GetFiles(layer_folder, "*", SearchOption.AllDirectories))
87 | {
88 | var relative = Path.GetRelativePath(layer_folder, file);
89 | bool include = true;
90 | foreach (var exclude in Exclude)
91 | {
92 | if (exclude.IsMatch(relative))
93 | {
94 | include = false;
95 | break;
96 | }
97 | }
98 |
99 | if (!include)
100 | {
101 | continue;
102 | }
103 | var dest = Path.Combine(output_folder, relative);
104 | var specs = MergingSpecs.Where(x => x.Matches(relative)).ToArray();
105 | Directory.CreateDirectory(Path.GetDirectoryName(dest));
106 | if (specs.Any())
107 | {
108 | foreach (var spec in specs)
109 | {
110 | spec.MergeFiles(dest, file);
111 | }
112 | }
113 | else
114 | Util.Copy(file, dest);
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/Util.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Encodings.Web;
2 | using System.Threading.Tasks;
3 |
4 | namespace MinecraftVersionHistory;
5 |
6 | public static class Util
7 | {
8 | public static IEnumerable OrderedChildren(this VersionNode node)
9 | {
10 | return node.Children.OrderBy(x => Depth(x));
11 | }
12 |
13 | public static string FilePath(string base_folder, YamlNode node, bool nullable = false)
14 | {
15 | if (nullable && node == null)
16 | return null;
17 | string path = (string)node;
18 | path = Environment.ExpandEnvironmentVariables(path);
19 | return Path.Combine(base_folder, path);
20 | }
21 |
22 | public static int Depth(this VersionNode node)
23 | {
24 | if (!node.Children.Any())
25 | return 1;
26 | return 1 + node.Children.Max(x => Depth(x));
27 | }
28 |
29 | public static string[] Split(string path)
30 | {
31 | return path.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
32 | }
33 |
34 | public static void Copy(string from, string to)
35 | {
36 | Directory.CreateDirectory(Path.GetDirectoryName(to));
37 | File.Copy(from, to, true);
38 | }
39 |
40 | private static T PathToThing(JsonObject root, Func create_default, params string[] path) where T : JsonNode
41 | {
42 | JsonNode start = root;
43 | foreach (var item in path)
44 | {
45 | start = start[item];
46 | if (start == null)
47 | return create_default();
48 | }
49 | return start as T ?? create_default();
50 | }
51 |
52 | public static JsonObject PathToObject(JsonObject root, params string[] path) => PathToThing(root, () => new JsonObject(), path);
53 | public static JsonArray PathToArray(JsonObject root, params string[] path) => PathToThing(root, () => new JsonArray(), path);
54 |
55 | public static void RemoveEmptyFolders(string root)
56 | {
57 | foreach (var directory in Directory.GetDirectories(root))
58 | {
59 | if (Path.GetFileName(directory) == ".git")
60 | continue;
61 | RemoveEmptyFolders(directory);
62 | if (Directory.GetFiles(directory).Length == 0 &&
63 | Directory.GetDirectories(directory).Length == 0)
64 | {
65 | Directory.Delete(directory, false);
66 | }
67 | }
68 | }
69 |
70 | public static string ToMinecraftJson(JsonNode value)
71 | {
72 | return value.ToJsonString(new JsonSerializerOptions()
73 | {
74 | WriteIndented = true,
75 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
76 | });
77 | }
78 |
79 | const int BYTES_TO_READ = sizeof(Int64);
80 | public static bool FilesAreEqual(FileInfo first, FileInfo second)
81 | {
82 | if (first.Length != second.Length)
83 | return false;
84 |
85 | if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
86 | return true;
87 |
88 | int iterations = (int)Math.Ceiling((double)first.Length / BYTES_TO_READ);
89 |
90 | using (FileStream fs1 = first.OpenRead())
91 | using (FileStream fs2 = second.OpenRead())
92 | {
93 | byte[] one = new byte[BYTES_TO_READ];
94 | byte[] two = new byte[BYTES_TO_READ];
95 |
96 | for (int i = 0; i < iterations; i++)
97 | {
98 | fs1.Read(one, 0, BYTES_TO_READ);
99 | fs2.Read(two, 0, BYTES_TO_READ);
100 |
101 | if (BitConverter.ToInt64(one, 0) != BitConverter.ToInt64(two, 0))
102 | return false;
103 | }
104 | }
105 |
106 | return true;
107 | }
108 |
109 | public static readonly HttpClient HTTP_CLIENT = new();
110 | public static void DownloadFile(string url, string path)
111 | {
112 | Profiler.Start($"Downloading {url} to {path}");
113 | Directory.CreateDirectory(Path.GetDirectoryName(path));
114 | using var stream = HTTP_CLIENT.GetStreamAsync(url).Result;
115 | using var file = File.Create(path);
116 | if (stream.CanSeek)
117 | stream.Seek(0, SeekOrigin.Begin);
118 | stream.CopyTo(file);
119 | Profiler.Stop();
120 | }
121 | public static string DownloadString(string url)
122 | {
123 | Profiler.Start($"Downloading {url}");
124 | string result = HTTP_CLIENT.GetStringAsync(url).Result;
125 | Profiler.Stop();
126 | return result;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | desktop.ini
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # DNX
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 |
50 | *_i.c
51 | *_p.c
52 | *_i.h
53 | *.ilk
54 | *.meta
55 | *.obj
56 | *.pch
57 | *.pdb
58 | *.pgc
59 | *.pgd
60 | *.rsp
61 | *.sbr
62 | *.tlb
63 | *.tli
64 | *.tlh
65 | *.tmp
66 | *.tmp_proj
67 | *.log
68 | *.vspscc
69 | *.vssscc
70 | .builds
71 | *.pidb
72 | *.svclog
73 | *.scc
74 |
75 | # Chutzpah Test files
76 | _Chutzpah*
77 |
78 | # Visual C++ cache files
79 | ipch/
80 | *.aps
81 | *.ncb
82 | *.opendb
83 | *.opensdf
84 | *.sdf
85 | *.cachefile
86 | *.VC.db
87 | *.VC.VC.opendb
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 | *.sap
94 |
95 | # TFS 2012 Local Workspace
96 | $tf/
97 |
98 | # Guidance Automation Toolkit
99 | *.gpState
100 |
101 | # ReSharper is a .NET coding add-in
102 | _ReSharper*/
103 | *.[Rr]e[Ss]harper
104 | *.DotSettings.user
105 |
106 | # JustCode is a .NET coding add-in
107 | .JustCode
108 |
109 | # TeamCity is a build add-in
110 | _TeamCity*
111 |
112 | # DotCover is a Code Coverage Tool
113 | *.dotCover
114 |
115 | # NCrunch
116 | _NCrunch_*
117 | .*crunch*.local.xml
118 | nCrunchTemp_*
119 |
120 | # MightyMoose
121 | *.mm.*
122 | AutoTest.Net/
123 |
124 | # Web workbench (sass)
125 | .sass-cache/
126 |
127 | # Installshield output folder
128 | [Ee]xpress/
129 |
130 | # DocProject is a documentation generator add-in
131 | DocProject/buildhelp/
132 | DocProject/Help/*.HxT
133 | DocProject/Help/*.HxC
134 | DocProject/Help/*.hhc
135 | DocProject/Help/*.hhk
136 | DocProject/Help/*.hhp
137 | DocProject/Help/Html2
138 | DocProject/Help/html
139 |
140 | # Click-Once directory
141 | publish/
142 |
143 | # Publish Web Output
144 | *.[Pp]ublish.xml
145 | *.azurePubxml
146 | # TODO: Comment the next line if you want to checkin your web deploy settings
147 | # but database connection strings (with potential passwords) will be unencrypted
148 | #*.pubxml
149 | *.publishproj
150 |
151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
152 | # checkin your Azure Web App publish settings, but sensitive information contained
153 | # in these scripts will be unencrypted
154 | PublishScripts/
155 |
156 | # NuGet Packages
157 | *.nupkg
158 | # The packages folder can be ignored because of Package Restore
159 | **/packages/*
160 | # except build/, which is used as an MSBuild target.
161 | !**/packages/build/
162 | # Uncomment if necessary however generally it will be regenerated when needed
163 | #!**/packages/repositories.config
164 | # NuGet v3's project.json files produces more ignoreable files
165 | *.nuget.props
166 | *.nuget.targets
167 |
168 | # Microsoft Azure Build Output
169 | csx/
170 | *.build.csdef
171 |
172 | # Microsoft Azure Emulator
173 | ecf/
174 | rcf/
175 |
176 | # Windows Store app package directories and files
177 | AppPackages/
178 | BundleArtifacts/
179 | Package.StoreAssociation.xml
180 | _pkginfo.txt
181 |
182 | # Visual Studio cache files
183 | # files ending in .cache can be ignored
184 | *.[Cc]ache
185 | # but keep track of directories ending in .cache
186 | !*.[Cc]ache/
187 |
188 | # Others
189 | ClientBin/
190 | ~$*
191 | *~
192 | *.dbmdl
193 | *.dbproj.schemaview
194 | *.jfm
195 | *.pfx
196 | *.publishsettings
197 | node_modules/
198 | orleans.codegen.cs
199 |
200 | # Since there are multiple workflows, uncomment next line to ignore bower_components
201 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
202 | #bower_components/
203 |
204 | # RIA/Silverlight projects
205 | Generated_Code/
206 |
207 | # Backup & report files from converting an old project file
208 | # to a newer Visual Studio version. Backup files are not needed,
209 | # because we have git ;-)
210 | _UpgradeReport_Files/
211 | Backup*/
212 | UpgradeLog*.XML
213 | UpgradeLog*.htm
214 |
215 | # SQL Server files
216 | *.mdf
217 | *.ldf
218 |
219 | # Business Intelligence projects
220 | *.rdl.data
221 | *.bim.layout
222 | *.bim_*.settings
223 |
224 | # Microsoft Fakes
225 | FakesAssemblies/
226 |
227 | # GhostDoc plugin setting file
228 | *.GhostDoc.xml
229 |
230 | # Node.js Tools for Visual Studio
231 | .ntvs_analysis.dat
232 |
233 | # Visual Studio 6 build log
234 | *.plg
235 |
236 | # Visual Studio 6 workspace options file
237 | *.opt
238 |
239 | # Visual Studio LightSwitch build output
240 | **/*.HTMLClient/GeneratedArtifacts
241 | **/*.DesktopClient/GeneratedArtifacts
242 | **/*.DesktopClient/ModelManifest.xml
243 | **/*.Server/GeneratedArtifacts
244 | **/*.Server/ModelManifest.xml
245 | _Pvt_Extensions
246 |
247 | # Paket dependency manager
248 | .paket/paket.exe
249 | paket-files/
250 |
251 | # FAKE - F# Make
252 | .fake/
253 |
254 | # JetBrains Rider
255 | .idea/
256 | *.sln.iml
257 |
258 | # CodeRush
259 | .cr/
260 |
261 | # Python Tools for Visual Studio (PTVS)
262 | __pycache__/
263 | *.pyc
--------------------------------------------------------------------------------
/MCPModernizer/MCP.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualBasic.FileIO;
2 | using MinecraftVersionHistory;
3 |
4 | namespace MCPModernizer;
5 |
6 | public abstract class MCP
7 | {
8 | public static readonly MCPSorter Sorter = new();
9 | public readonly Sided FriendlyNames = new();
10 | public readonly Sided LocalMappings = new();
11 | public string? ClientVersion { get; protected set; }
12 |
13 | public void WriteClientMappings(string path)
14 | {
15 | using var writer = new StreamWriter(path);
16 | MappingsIO.WriteTsrg(LocalMappings.Client, writer);
17 | }
18 | public void WriteServerMappings(string path)
19 | {
20 | using var writer = new StreamWriter(path);
21 | MappingsIO.WriteTsrg(LocalMappings.Server, writer);
22 | }
23 |
24 | protected void ParseCSVs(StreamReader? classes, StreamReader? methods, StreamReader? fields)
25 | {
26 | if (classes != null)
27 | {
28 | var class_list = ParseCSV(classes).ToList();
29 | if (class_list[0][0] == "name")
30 | {
31 | // 3.0 - 5.6 style
32 | foreach (var item in class_list.Skip(1))
33 | {
34 | // skip lines with no destination package (a few random ones that clearly aren't classes)
35 | if (item[3] != "")
36 | AddToSide(item[4], LocalMappings, x => x.AddClass(item[1], item[3].Replace('/', '.') + '.' + item[0]));
37 | }
38 | }
39 | }
40 | if (methods != null)
41 | {
42 | var method_list = ParseCSV(methods).ToList();
43 | if (method_list[0].Length == 4)
44 | {
45 | // 6.0+ style
46 | foreach (var item in method_list.Skip(1))
47 | {
48 | AddToSide(item[2], FriendlyNames, x => x.AddMethod(item[0], item[1]));
49 | }
50 | }
51 | else if (method_list[0].Length == 9)
52 | {
53 | // 3.0 - 5.6 style
54 | foreach (var item in method_list.Skip(1))
55 | {
56 | AddToSide(item[8], LocalMappings, x => x.GetOrAddClass(item[6].Replace('/', '.')).AddMethod(item[2], item[0], item[4]));
57 | AddToSide(item[8], FriendlyNames, x => x.AddMethod(item[0], item[1]));
58 | }
59 | }
60 | else
61 | {
62 | // 2.0 - 2.12 style
63 | // has some weird entries at the end we need to skip
64 | foreach (var item in method_list.Skip(4).TakeWhile(x => x.Length >= 5))
65 | {
66 | if (item[1] != "*" && item[1] != "")
67 | FriendlyNames.Client.AddMethod(item[1], item[4]);
68 | if (item[3] != "*" && item[3] != "")
69 | FriendlyNames.Server.AddMethod(item[3], item[4]);
70 | }
71 | }
72 | }
73 | if (fields != null)
74 | {
75 | var field_list = ParseCSV(fields).ToList();
76 | if (field_list[0].Length == 4)
77 | {
78 | // 6.0+ style
79 | foreach (var item in field_list.Skip(1))
80 | {
81 | // fix some modern ones prefixing fields
82 | var name = item[0];
83 | int dot = name.IndexOf('.');
84 | if (dot != -1)
85 | name = name[(dot + 1)..];
86 | AddToSide(item[2], FriendlyNames, x => x.AddField(name, item[1]));
87 | }
88 | }
89 | else if (field_list[0].Length == 9)
90 | {
91 | // 3.0 - 5.6 style
92 | foreach (var item in field_list.Skip(1))
93 | {
94 | AddToSide(item[8], LocalMappings, x => x.GetOrAddClass(item[6].Replace('/', '.')).AddField(item[2], item[0]));
95 | AddToSide(item[8], FriendlyNames, x => x.AddField(item[0], item[1]));
96 | }
97 | }
98 | else
99 | {
100 | // 2.0 - 2.12 style
101 | foreach (var item in field_list.Skip(3))
102 | {
103 | if (item[2] != "*" && item[2] != "")
104 | FriendlyNames.Client.AddField(item[2], item[6]);
105 | if (item[5] != "*" && item[5] != "")
106 | FriendlyNames.Server.AddField(item[5], item[6]);
107 | }
108 | }
109 | }
110 | }
111 |
112 | private void AddToSide(string side, Sided sided, Action action) where T : new()
113 | {
114 | if (side == "0" || side == "2")
115 | action(sided.Client);
116 | if (side == "1" || side == "2")
117 | action(sided.Server);
118 | }
119 |
120 | protected IEnumerable ParseCSV(StreamReader reader)
121 | {
122 | var parser = new TextFieldParser(reader);
123 | parser.HasFieldsEnclosedInQuotes = true;
124 | parser.SetDelimiters(",");
125 | while (!parser.EndOfData)
126 | {
127 | yield return parser.ReadFields()!;
128 | }
129 | }
130 | }
131 |
132 | public class MCPSorter : IComparer
133 | {
134 | public int Compare(MCP? x, MCP? y)
135 | {
136 | if (x is ClassicMCP cx && y is ClassicMCP cy)
137 | {
138 | int m = cx.MajorVersion.CompareTo(cy.MajorVersion);
139 | if (m != 0)
140 | return m;
141 | int m2 = cx.MinorVersion.CompareTo(cy.MinorVersion);
142 | if (m2 != 0)
143 | return m2;
144 | return String.Compare(cx.ExtraVersion, cy.ExtraVersion, StringComparison.Ordinal);
145 | }
146 | return 0;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Bedrock/MergingSpec.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class MergingSpec
4 | {
5 | private readonly string Extension;
6 | private readonly string[] Path;
7 | private readonly ForwardNodeFinder ListPath;
8 | private readonly List OverwriteKeys;
9 | public readonly MergeOperation Operation;
10 | public readonly KeyMover KeyMover;
11 | public MergingSpec(YamlMappingNode node)
12 | {
13 | var path_node = node.TryGet("path");
14 | if (path_node != null)
15 | Path = Util.Split((string)path_node);
16 | var list_path_node = node.TryGet("list");
17 | if (list_path_node != null)
18 | ListPath = new ForwardNodeFinder(list_path_node.ToList(NodeMatcher.Create));
19 | var ext_node = node.TryGet("extension");
20 | if (ext_node != null)
21 | {
22 | Extension = (string)ext_node;
23 | if (!Extension.StartsWith('.'))
24 | Extension = "." + Extension;
25 | }
26 | OverwriteKeys = node.Go("overwrite").ToStringList() ?? new List();
27 | var operation_node = node.TryGet("operation");
28 | if (operation_node == null)
29 | Operation = MergeOperation.MergeJson;
30 | else
31 | Operation = StringUtils.ParseUnderscoredEnum((string)operation_node);
32 | KeyMover = node.Go("move_keys").NullableParse(x => new KeyMover((YamlMappingNode)x));
33 | }
34 |
35 | public bool Matches(string path)
36 | {
37 | if (Path != null)
38 | {
39 | var split = Util.Split(path);
40 | if (split.Length < Path.Length)
41 | return false;
42 | for (int i = 0; i < Path.Length; i++)
43 | {
44 | if (split[i] != Path[i])
45 | return false;
46 | }
47 | }
48 | if (Extension != null)
49 | {
50 | if (System.IO.Path.GetExtension(path) != Extension)
51 | return false;
52 | }
53 | return true;
54 | }
55 |
56 | public void MergeFiles(string current_path, string newer_path)
57 | {
58 | if (Operation == MergeOperation.MergeJson)
59 | {
60 | var newer = JsonNode.Parse(File.ReadAllText(newer_path), null, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip });
61 | if (KeyMover != null && newer is JsonObject obj)
62 | KeyMover.MoveKeys(obj);
63 | JsonNode result = newer;
64 | if (File.Exists(current_path))
65 | {
66 | result = JsonNode.Parse(File.ReadAllText(current_path));
67 | TopLevelMerge(result, newer);
68 | }
69 | File.WriteAllText(current_path, Util.ToMinecraftJson(result));
70 | }
71 | else if (Operation == MergeOperation.AppendList)
72 | {
73 | var newer = JsonNode.Parse(File.ReadAllText(newer_path), null, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip });
74 | JsonNode result = newer;
75 | if (File.Exists(current_path))
76 | {
77 | result = JsonNode.Parse(File.ReadAllText(current_path));
78 | var result_lists = ListPath.FindNodes(result).Select(x => x.node).OfType().ToList();
79 | var newer_lists = ListPath.FindNodes(newer).Select(x => x.node).OfType().ToList();
80 | foreach (var r in result_lists)
81 | {
82 | foreach (var n in newer_lists)
83 | {
84 | MergeTopArray(r, n);
85 | }
86 | }
87 | }
88 | File.WriteAllText(current_path, Util.ToMinecraftJson(result));
89 | }
90 | else if (Operation == MergeOperation.AppendLines)
91 | {
92 | if (File.Exists(current_path))
93 | {
94 | using var input = File.OpenRead(newer_path);
95 | using var output = new FileStream(current_path, FileMode.Append, FileAccess.Write, FileShare.None);
96 | output.Write(Encoding.UTF8.GetBytes(Environment.NewLine));
97 | input.CopyTo(output);
98 | }
99 | else
100 | File.Copy(newer_path, current_path);
101 | }
102 | else if (Operation == MergeOperation.NoMerge)
103 | {
104 | if (!File.Exists(current_path))
105 | File.Copy(newer_path, current_path);
106 | }
107 | }
108 |
109 | public void TopLevelMerge(JsonNode current, JsonNode newer)
110 | {
111 | if (current is JsonObject cj && newer is JsonObject nj)
112 | MergeObjects(cj, nj);
113 | else if (current is JsonArray ca && newer is JsonArray na)
114 | MergeTopArray(ca, na);
115 | }
116 |
117 | public void MergeObjects(JsonObject current, JsonObject newer)
118 | {
119 | foreach (var item in newer)
120 | {
121 | bool exists = current.TryGetPropertyValue(item.Key, out var existing);
122 | if (!exists || OverwriteKeys.Contains(item.Key))
123 | current[item.Key] = JsonNode.Parse(item.Value.ToJsonString());
124 | else
125 | {
126 | if (existing is JsonObject j1 && item.Value is JsonObject j2)
127 | MergeObjects(j1, j2);
128 | else
129 | current[item.Key] = JsonNode.Parse(item.Value.ToJsonString());
130 | }
131 | }
132 | }
133 |
134 | public void MergeTopArray(JsonArray current, JsonArray newer)
135 | {
136 | foreach (var sub in newer)
137 | {
138 | current.Add(JsonNode.Parse(sub.ToJsonString()));
139 | }
140 | }
141 | }
142 |
143 | public enum MergeOperation
144 | {
145 | AppendLines,
146 | MergeJson,
147 | AppendList,
148 | NoMerge
149 | }
150 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/MappingsIO.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace MinecraftVersionHistory;
3 |
4 | public static class MappingsIO
5 | {
6 | public static void ParseTsrg(Mappings mappings, StreamReader reader)
7 | {
8 | MappedClass current_class = null;
9 | while (!reader.EndOfStream)
10 | {
11 | var line = reader.ReadLine();
12 | if (String.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
13 | continue;
14 | if (line.StartsWith('\t'))
15 | {
16 | var entries = line.TrimStart('\t').Split(' ');
17 | if (entries.Length == 2)
18 | current_class.AddField(entries[0], entries[1]);
19 | else if (entries.Length == 3)
20 | current_class.AddMethod(entries[0], entries[2], entries[1]);
21 | }
22 | else
23 | {
24 | var entries = line.Split(' ');
25 | current_class = mappings.AddClass(entries[0].Replace('/', '.'), entries[1].Replace('/', '.'));
26 | }
27 | }
28 | }
29 |
30 | public static void ParseSrg(Mappings mappings, StreamReader reader)
31 | {
32 | while (!reader.EndOfStream)
33 | {
34 | var line = reader.ReadLine();
35 | if (String.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
36 | continue;
37 | var entries = line.Split(' ');
38 | var type = entries[0];
39 | if (type == "CL:")
40 | mappings.AddClass(entries[1].Replace('/', '.'), entries[2].Replace('/', '.'));
41 | else if (type == "FD:")
42 | {
43 | var (cpath, name) = Split(entries[1]);
44 | mappings.GetOrAddClass(cpath).AddField(name, Split(entries[2]).name);
45 | }
46 | else if (type == "MD:")
47 | {
48 | var (cpath, name) = Split(entries[1]);
49 | mappings.GetOrAddClass(cpath).AddMethod(name, Split(entries[3]).name, entries[2]);
50 | }
51 | }
52 | }
53 |
54 | public static void WriteTsrg(Mappings mappings, StreamWriter writer)
55 | {
56 | foreach (var c in mappings.ClassList)
57 | {
58 | writer.WriteLine($"{c.OldName.Replace('.', '/')} {c.NewName.Replace('.', '/')}");
59 | foreach (var f in c.FieldList)
60 | {
61 | writer.WriteLine($"\t{f.OldName} {f.NewName}");
62 | }
63 | foreach (var m in c.MethodList)
64 | {
65 | writer.WriteLine($"\t{m.OldName} {m.Signature} {m.NewName}");
66 | }
67 | }
68 | }
69 |
70 | public static (string classpath, string name) Split(string path)
71 | {
72 | int sep = path.LastIndexOf('/');
73 | if (sep == -1)
74 | return (null, path);
75 | return (path[..sep].Replace('/', '.'), path[(sep + 1)..]);
76 | }
77 |
78 | public static void ParseProguard(Mappings mappings, StreamReader reader)
79 | {
80 | var methods = new List<(MappedClass add_to, string from, string to, string ret, string[] args)>();
81 | var backwards_classes = new Dictionary();
82 | MappedClass current_class = null;
83 | while (!reader.EndOfStream)
84 | {
85 | var line = reader.ReadLine();
86 | if (String.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
87 | continue;
88 | if (line.StartsWith(" "))
89 | {
90 | var entries = line.TrimStart(' ').Split(" -> ");
91 | if (entries[0].Contains('('))
92 | {
93 | var split = entries[0].Split(' ');
94 | int open = split[1].IndexOf('(');
95 | int close = split[1].IndexOf(')');
96 | string name = split[1][..open];
97 | int colon = split[0].LastIndexOf(':') + 1;
98 | methods.Add((current_class, entries[1], name, split[0][colon..], split[1][(open + 1)..close].Split(',', StringSplitOptions.RemoveEmptyEntries)));
99 | }
100 | else
101 | current_class.AddField(entries[1], entries[0].Split(' ')[1]);
102 | }
103 | else
104 | {
105 | var entries = line.Split(" -> ");
106 | string from = entries[1].TrimEnd(':');
107 | current_class = mappings.AddClass(from, entries[0]);
108 | backwards_classes[entries[0]] = from;
109 | }
110 | }
111 | string shorten(string identifier)
112 | {
113 | (int count, string rest) = TrimArrays(identifier);
114 | return Shorthand(backwards_classes.GetValueOrDefault(rest, rest), count);
115 | }
116 | foreach (var (add_to, from, to, ret, args) in methods)
117 | {
118 | string signature = $"({String.Join("", args.Select(shorten))}){shorten(ret)}";
119 | add_to.AddMethod(from, to, signature);
120 | }
121 | }
122 |
123 | private static (int count, string rest) TrimArrays(string identifier)
124 | {
125 | int arrays = 0;
126 | while (identifier.EndsWith("[]"))
127 | {
128 | arrays++;
129 | identifier = identifier[0..^2];
130 | }
131 | return (arrays, identifier);
132 | }
133 |
134 | private static string Shorthand(string identifier, int arrays)
135 | {
136 | identifier = identifier switch
137 | {
138 | "int" => "I",
139 | "double" => "D",
140 | "boolean" => "Z",
141 | "float" => "F",
142 | "long" => "J",
143 | "byte" => "B",
144 | "short" => "S",
145 | "char" => "C",
146 | "void" => "V",
147 | _ => $"L{identifier.Replace('.', '/')};"
148 | };
149 | return new String('[', arrays) + identifier;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Utilities/NbtTranslationOptions.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class NbtTranslationOptions
4 | {
5 | public readonly string Extension;
6 | public readonly string NewExtension;
7 | public readonly Endianness Endianness;
8 | public readonly SnbtOptions Options;
9 | public readonly bool ConvertStructure;
10 | private readonly List RemoveKeys;
11 | private readonly bool Mini;
12 | public NbtTranslationOptions(YamlMappingNode node)
13 | {
14 | Extension = (string)node["extension"];
15 | if (!Extension.StartsWith('.'))
16 | Extension = "." + Extension;
17 | RemoveKeys = node.Go("remove keys").ToStringList() ?? new List();
18 | Endianness = node.Go("endian").ToEnum() ?? Endianness.Big;
19 | NewExtension = node.TryGet("new extension").String() ?? ".snbt";
20 | Mini = node.Go("minified").NullableStructParse(x => Boolean.Parse(x.String())) ?? false;
21 | Options = Mini ? SnbtOptions.Default : SnbtOptions.DefaultExpanded;
22 | ConvertStructure = node.Go("structure").NullableStructParse(x => Boolean.Parse(x.String())) ?? false;
23 | }
24 |
25 | public bool ShouldTranslate(string path)
26 | {
27 | return Path.GetExtension(path) == Extension;
28 | }
29 |
30 | public void Translate(string path)
31 | {
32 | var file = new NbtFile();
33 | if (Endianness == Endianness.Little)
34 | file.BigEndian = false;
35 | file.LoadFromFile(path);
36 | var root = (NbtCompound)file.RootTag;
37 | if (RemoveKeys.Any())
38 | {
39 | foreach (var key in RemoveKeys)
40 | {
41 | root.Remove(key);
42 | }
43 | file.SaveToFile(path, file.FileCompression);
44 | }
45 | if (ConvertStructure)
46 | {
47 | var palettes = root.Get("palettes");
48 | var palette = palettes != null ? (NbtList)palettes[0] : root.Get("palette");
49 | NbtList new_palette = null;
50 | if (palette != null)
51 | {
52 | new_palette = new NbtList(palette.Tags.Cast().Select(PackBlockState));
53 | root["palette"] = new_palette;
54 | if (palettes != null)
55 | {
56 | var new_palettes = new NbtList();
57 | foreach (NbtList p in palettes)
58 | {
59 | var compound = new NbtCompound();
60 | for (int i = 0; i < palettes.Count; i++)
61 | {
62 | compound[i.ToString()] = PackBlockState((NbtCompound)p[i]);
63 | }
64 | }
65 | root["palettes"] = new_palettes;
66 | }
67 | }
68 | var entities = root.Get("entities");
69 | if (entities != null)
70 | {
71 | var sorted = entities.Tags.Cast()
72 | .OrderBy(x => x.Get("pos")[1].DoubleValue)
73 | .ThenBy(x => x.Get("pos")[0].DoubleValue)
74 | .ThenBy(x => x.Get("pos")[2].DoubleValue).ToList();
75 | entities.Clear();
76 | foreach (var entity in sorted)
77 | {
78 | entity.Sort(new SpecificOrder("blockPos", "pos", "nbt"), false);
79 | entity.Get("nbt").Sort(new SpecificOrder("id"), false);
80 | }
81 | root["entities"] = new NbtList(sorted);
82 | }
83 | var blocks = root.Get("blocks");
84 | var new_blocks = blocks.Tags.Cast()
85 | .OrderBy(x => x.Get("pos")[1].IntValue)
86 | .ThenBy(x => x.Get("pos")[0].IntValue)
87 | .ThenBy(x => x.Get("pos")[2].IntValue).ToList();
88 | blocks.Clear();
89 | foreach (var block in new_blocks)
90 | {
91 | if (new_palette != null)
92 | {
93 | var state = block.Get("state");
94 | block["state"] = (NbtTag)new_palette[state.Value].Clone();
95 | }
96 | block.Sort(new SpecificOrder("pos", "state", "nbt"), false);
97 | }
98 | // overwrite existing, then rename
99 | root["blocks"] = new NbtList(new_blocks);
100 | root["blocks"].Name = "data";
101 | }
102 | root.Sort(new SpecificOrder("size", "data", "entities", "palette"), false);
103 | if (!Mini)
104 | {
105 | Options.ShouldIndent = tag =>
106 | {
107 | if (tag.Parent == null || tag.Parent == root)
108 | return true;
109 | var top_parent = tag;
110 | while (top_parent.Parent != root)
111 | {
112 | top_parent = top_parent.Parent;
113 | }
114 | if (top_parent == root["data"])
115 | return false;
116 | if (top_parent == root["entities"])
117 | return false;
118 | return true;
119 | };
120 | }
121 | File.WriteAllText(Path.ChangeExtension(path, NewExtension), root.ToSnbt(Options) + Environment.NewLine);
122 | }
123 |
124 | public class SpecificOrder : IComparer
125 | {
126 | public readonly string[] Order;
127 | public SpecificOrder(params string[] order)
128 | {
129 | Order = order;
130 | }
131 | public int Compare(NbtTag? x, NbtTag? y)
132 | {
133 | return Index(x.Name).CompareTo(Index(y.Name));
134 | }
135 | private int Index(string name)
136 | {
137 | int index = Array.IndexOf(Order, name);
138 | if (index == -1)
139 | return int.MaxValue;
140 | return index;
141 | }
142 | }
143 |
144 | // differs slightly from the Minecraft implementation
145 | // emits block states in the form [key=value] instead of {key:value}
146 | private NbtString PackBlockState(NbtCompound compound)
147 | {
148 | var builder = new StringBuilder(compound.Get("Name").Value);
149 | var properties = compound.Get("Properties");
150 | if (properties != null)
151 | {
152 | string str = String.Join(',', properties.Select(x => x.Name + '=' + x.StringValue));
153 | builder.Append('[').Append(str).Append(']');
154 | }
155 | return new NbtString(builder.ToString());
156 | }
157 | }
158 |
159 | public enum Endianness
160 | {
161 | Big,
162 | Little
163 | }
164 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Abstract/VersionFacts.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class VersionFacts : IComparer
4 | {
5 | private readonly List SkipVersions;
6 | private readonly List InsaneBranches;
7 | private readonly List InsaneVersions;
8 | private readonly Dictionary ParentsMap;
9 | private readonly Dictionary CustomNameMap;
10 | private readonly Dictionary RegexReleases;
11 | private readonly List SnapshotReleases;
12 | private readonly List VersionOrder;
13 | private readonly List OrderPriority;
14 | public VersionFacts(YamlMappingNode yaml)
15 | {
16 | SkipVersions = yaml.Go("skip").ToList(x => new Regex((string)x)) ?? new();
17 | InsaneBranches = yaml.Go("insane", "releases").ToList(x => new Regex((string)x)) ?? new();
18 | InsaneVersions = yaml.Go("insane", "versions").ToList(x => new Regex((string)x)) ?? new();
19 | ParentsMap = yaml.Go("parents").ToDictionary() ?? new();
20 | CustomNameMap = yaml.Go("names").ToDictionary() ?? new();
21 | RegexReleases = yaml.Go("releases", "regex").ToDictionary(x => new Regex((string)x), x => (string)x) ?? new();
22 | SnapshotReleases = yaml.Go("releases", "snapshots").ToList(x => new SnapshotSpec((YamlMappingNode)x)) ?? new();
23 | VersionOrder = yaml.Go("ordering", "versions").ToStringList() ?? new();
24 | OrderPriority = yaml.Go("ordering", "priority").ToList(x => StringUtils.ParseUnderscoredEnum((string)x)) ?? new() { OrderMethod.ListedOrder, OrderMethod.ReleaseTime, OrderMethod.BestGuess };
25 | }
26 |
27 | public bool ShouldSkip(Version version)
28 | {
29 | foreach (var item in SkipVersions)
30 | {
31 | if (item.IsMatch(version.Name))
32 | return true;
33 | }
34 | return false;
35 | }
36 |
37 | public string? CustomName(string file)
38 | {
39 | CustomNameMap.TryGetValue(file, out var result);
40 | return result;
41 | }
42 |
43 | public bool IsInsaneRelease(string release)
44 | {
45 | return InsaneBranches.Any(x => x.IsMatch(release));
46 | }
47 |
48 | public bool IsInsaneVersion(Version version)
49 | {
50 | return InsaneVersions.Any(x => x.IsMatch(version.Name));
51 | }
52 |
53 | public string GetReleaseName(Version version)
54 | {
55 | foreach (var candidate in RegexReleases)
56 | {
57 | if (candidate.Key.IsMatch(version.Name))
58 | return candidate.Key.Replace(version.Name, candidate.Value);
59 | }
60 | if (SnapshotSpec.IsSnapshot(version, out var snap))
61 | {
62 | foreach (var candidate in SnapshotReleases)
63 | {
64 | if (candidate.Matches(snap))
65 | return candidate.Release;
66 | }
67 | }
68 | throw new KeyNotFoundException($"What release is {version} for?");
69 | }
70 |
71 | public string SpecialParent(Version version)
72 | {
73 | ParentsMap.TryGetValue(version.Name, out var result);
74 | return result;
75 | }
76 |
77 | private enum OrderMethod
78 | {
79 | ReleaseTime,
80 | BestGuess,
81 | ListedOrder
82 | }
83 |
84 | public int Compare(Version? x, Version? y)
85 | {
86 | if (x == y)
87 | return 0;
88 | foreach (var method in OrderPriority)
89 | {
90 | int order = 0;
91 | if (method == OrderMethod.BestGuess)
92 | order = BestGuessCompare(x, y);
93 | else if (method == OrderMethod.ReleaseTime)
94 | order = x.ReleaseTime.CompareTo(y.ReleaseTime);
95 | else if (method == OrderMethod.ListedOrder)
96 | {
97 | int x_index = VersionOrder.IndexOf(x.Name);
98 | int y_index = VersionOrder.IndexOf(y.Name);
99 | if (x_index != -1 && y_index != -1)
100 | order = x_index.CompareTo(y_index);
101 | }
102 | if (order != 0)
103 | return order;
104 | }
105 | throw new ArgumentException($"Can't tell which came first: {x} or {y}");
106 | }
107 |
108 | private static readonly char[] TypicalSplits = new char[] { ' ', '-', '.', '_' };
109 | private int BestGuessCompare(Version n1, Version n2)
110 | {
111 | bool is_snap1 = SnapshotSpec.IsSnapshot(n1, out var snap1);
112 | bool is_snap2 = SnapshotSpec.IsSnapshot(n2, out var snap2);
113 | // if they're both snapshots, compare snapshot data
114 | if (is_snap1 && is_snap2)
115 | {
116 | int year_compare = snap1.Year.CompareTo(snap2.Year);
117 | if (year_compare != 0)
118 | return year_compare;
119 | int week_compare = snap1.Week.CompareTo(snap2.Week);
120 | if (week_compare != 0)
121 | return week_compare;
122 | int sub_compare = snap1.Subversion.CompareTo(snap2.Subversion);
123 | if (sub_compare != 0)
124 | return sub_compare;
125 | }
126 | // assume releases always follow snapshots
127 | if (is_snap1 && !is_snap2)
128 | return 1;
129 | if (is_snap2 && !is_snap1)
130 | return -1;
131 | string[] n1_split = n1.Name.Split(TypicalSplits, StringSplitOptions.RemoveEmptyEntries);
132 | string[] n2_split = n2.Name.Split(TypicalSplits, StringSplitOptions.RemoveEmptyEntries);
133 | int[] n1_nums = n1_split.Select(x => FindNumber(x)).ToArray();
134 | int[] n2_nums = n2_split.Select(x => FindNumber(x)).ToArray();
135 | for (int i = 0; i < Math.Min(n1_nums.Length, n2_nums.Length); i++)
136 | {
137 | if (n1_nums[i] == -1 && n2_nums[i] == -1)
138 | continue;
139 | if (n1_nums[i] == -1)
140 | return 1;
141 | if (n2_nums[i] == -1)
142 | return -1;
143 | int compare = n1_nums[i].CompareTo(n2_nums[i]);
144 | if (compare != 0)
145 | return compare;
146 | }
147 | for (int i = 0; i < Math.Min(n1_split.Length, n2_split.Length); i++)
148 | {
149 | int compare = n1_split[i].CompareTo(n2_split[i]);
150 | if (compare != 0)
151 | return compare;
152 | }
153 | // assume shorter strings come first, e.g. 1.2.3-ex > 1.2.3
154 | int size_compare = n1_split.Length.CompareTo(n2_split.Length);
155 | if (size_compare != 0)
156 | return size_compare;
157 | return 0;
158 | }
159 |
160 | private static readonly Regex NumberFinder = new(@"\d+");
161 | private static int FindNumber(string str)
162 | {
163 | var match = NumberFinder.Match(str);
164 | if (!match.Success)
165 | return -1;
166 | return int.Parse(match.Value);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/JavaConfig.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class JavaConfig : VersionConfig
4 | {
5 | public readonly List JavaInstallationPaths;
6 | public readonly string FernflowerPath;
7 | public readonly string CfrPath;
8 | public readonly string SpecialSourcePath;
9 | public readonly string ServerJarFolder;
10 | public readonly string AssetsFolder;
11 | public readonly string DecompilerArgs;
12 | public readonly RetroMCP MCP;
13 | public readonly string CfrArgs;
14 | public readonly string FernflowerArgs;
15 | public readonly DateTime DataGenerators;
16 | public readonly DecompilerType? Decompiler;
17 | private readonly List JsonSorters;
18 | private readonly List ExcludeJarEntries;
19 | private readonly List ExcludeDecompiledEntries;
20 | public JavaConfig(string folder, AppConfig parent, YamlMappingNode yaml) : base(folder, parent, yaml)
21 | {
22 | JavaInstallationPaths = yaml.Go("java install").ToList(x => Util.FilePath(folder, x));
23 | FernflowerPath = Util.FilePath(folder, yaml["fernflower jar"]);
24 | CfrPath = Util.FilePath(folder, yaml["cfr jar"]);
25 | SpecialSourcePath = Util.FilePath(folder, yaml["special source jar"]);
26 | ServerJarFolder = Util.FilePath(folder, yaml["server jars"]);
27 | AssetsFolder = Util.FilePath(folder, yaml["assets folder"]);
28 | string mcp = Util.FilePath(folder, yaml.Go("mcp", "merged"));
29 | string ver = yaml.Go("mcp", "matched").String();
30 | if (mcp != null && ver != null)
31 | MCP = new RetroMCP(mcp, ver);
32 | Decompiler = ParseDecompiler((string)yaml["decompiler"]);
33 | DecompilerArgs = (string)yaml["decompiler args"];
34 | CfrArgs = (string)yaml["cfr args"];
35 | FernflowerArgs = (string)yaml["fernflower args"];
36 | DataGenerators = DateTime.Parse((string)yaml["data generators"]);
37 | JsonSorters = new();
38 | foreach (YamlMappingNode entry in (YamlSequenceNode)yaml.Go("json sorting"))
39 | {
40 | var files = entry["file"];
41 | var list = new List();
42 | if (files is YamlScalarNode single)
43 | list.Add(single.Value);
44 | else
45 | list.AddRange(((YamlSequenceNode)files).ToStringList());
46 | var sort = JsonSorterFactory.Create(entry["sort"]);
47 | var require = entry.Go("require").NullableParse(x => new SorterRequirements((YamlMappingNode)x)) ?? new SorterRequirements();
48 | JsonSorters.Add(new FileSorter(list.ToArray(), sort, require));
49 | }
50 | ExcludeJarEntries = yaml.Go("jar exclude").ToList(x => new Regex((string)x)) ?? new();
51 | ExcludeDecompiledEntries = yaml.Go("decompile exclude").ToList(x => new Regex((string)x)) ?? new();
52 | }
53 |
54 | public Sided GetMCPMappings(JavaVersion version)
55 | {
56 | if (MCP == null)
57 | return null;
58 | return MCP.CreateMappings(version.Name);
59 | }
60 |
61 | protected override VersionFacts CreateVersionFacts(YamlMappingNode yaml)
62 | {
63 | return new VersionFacts(yaml);
64 | }
65 |
66 | private static readonly string[] IllegalNames = new[] { "aux", "con", "nul", "prn", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" };
67 | public bool ExcludeJarEntry(string name)
68 | {
69 | if (IllegalNames.Contains(Path.GetFileNameWithoutExtension(name).ToLower()))
70 | return true;
71 | if (ExcludeJarEntries.Any(x => x.IsMatch(name)))
72 | return true;
73 | return false;
74 | }
75 | public bool ExcludeDecompiledEntry(string name)
76 | {
77 | if (ExcludeDecompiledEntries.Any(x => x.IsMatch(name)))
78 | return true;
79 | return false;
80 | }
81 |
82 | public void RemapJar(string in_path, string mappings_path, string out_path)
83 | {
84 | Profiler.Start("Remapping jar with SpecialSource");
85 | var result = CommandRunner.RunJavaCommand(Path.GetDirectoryName(out_path), JavaInstallationPaths, $"-jar \"{SpecialSourcePath}\" " +
86 | $"--in-jar \"{in_path}\" --out-jar \"{out_path}\" --srg-in \"{mappings_path}\" --kill-lvt");
87 | Profiler.Stop();
88 | if (result.ExitCode != 0)
89 | throw new InvalidOperationException($"SpecialSource failed with exit code {result.ExitCode}: {result.Error}");
90 | }
91 |
92 | public void JsonSort(string folder, Version version)
93 | {
94 | foreach (var sorter in JsonSorters)
95 | {
96 | if (!sorter.Requirements.MetBy(version))
97 | continue;
98 | foreach (var f in sorter.Files)
99 | {
100 | var path = Path.Combine(folder, f);
101 | if (File.Exists(path))
102 | {
103 | Console.WriteLine($"Sorting {f}");
104 | if (!sorter.Requirements.MetBy(path))
105 | continue;
106 | SortJsonFile(path, sorter.Sorter);
107 | }
108 | else if (Directory.Exists(path))
109 | {
110 | var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
111 | Console.WriteLine($"Sorting {files.Length} files in {f}");
112 | foreach (var sub in files)
113 | {
114 | if (!sorter.Requirements.MetBy(sub))
115 | continue;
116 | SortJsonFile(sub, sorter.Sorter);
117 | }
118 | }
119 | else
120 | Console.WriteLine($"Not sorting {f}, no file found");
121 | }
122 | }
123 | }
124 |
125 | private void SortJsonFile(string path, IJsonSorter sorter)
126 | {
127 | try
128 | {
129 | var json = JsonNode.Parse(File.ReadAllText(path), documentOptions: new JsonDocumentOptions() { });
130 | sorter.Sort(json);
131 | File.WriteAllText(path, Util.ToMinecraftJson(json));
132 | }
133 | catch (Exception ex)
134 | {
135 | Console.WriteLine($"Couldn't sort {path}: {ex.Message}");
136 | }
137 | }
138 |
139 | private static DecompilerType? ParseDecompiler(string input)
140 | {
141 | if (String.Equals(input, "fernflower", StringComparison.OrdinalIgnoreCase))
142 | return DecompilerType.Fernflower;
143 | if (String.Equals(input, "cfr", StringComparison.OrdinalIgnoreCase))
144 | return DecompilerType.Cfr;
145 | if (String.Equals(input, "fernflower_unzipped", StringComparison.OrdinalIgnoreCase))
146 | return DecompilerType.FernflowerUnzipped;
147 | return null;
148 | }
149 | }
150 |
151 | public enum DecompilerType
152 | {
153 | Fernflower,
154 | FernflowerUnzipped,
155 | Cfr
156 | }
157 |
--------------------------------------------------------------------------------
/MCPModernizer/ClassicMCP.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualBasic.FileIO;
2 | using MinecraftVersionHistory;
3 | using System.IO.Compression;
4 | using System.Text.RegularExpressions;
5 |
6 | namespace MCPModernizer;
7 |
8 | public class ClassicMCP : MCP
9 | {
10 | public readonly string ZipPath;
11 | public readonly int MajorVersion;
12 | public readonly int MinorVersion;
13 | public readonly string ExtraVersion;
14 | public readonly Sided> NewIDs = new();
15 | public string Version => $"{MajorVersion}.{MinorVersion}{ExtraVersion}";
16 | private static readonly Regex MCPVersionRegex = new(@"mcp(?\d)(?\d+)(?.*)");
17 | private static readonly Regex RevengVersionRegex = new(@"revengpack(?\d)(?\d+)(?.*)");
18 | private static readonly Regex ClientVersionRegex = new(@"ClientVersion = (?.*)");
19 | private static readonly Regex ServerVersionRegex = new(@"ServerVersion = (?.*)");
20 | private static readonly Regex ReadMeRegex = new(@"Minecraft mod creator pack (.*?) for Minecraft (?.*)");
21 | public ClassicMCP(string path, Dictionary version_fallback)
22 | {
23 | ZipPath = path;
24 | using ZipArchive zip = ZipFile.OpenRead(path);
25 | var v = MCPVersionRegex.Match(Path.GetFileNameWithoutExtension(path));
26 | if (!v.Success)
27 | v = RevengVersionRegex.Match(Path.GetFileNameWithoutExtension(path));
28 | MajorVersion = int.Parse(v.Groups["lead"].Value);
29 | MinorVersion = int.Parse(v.Groups["digits"].Value);
30 | ExtraVersion = v.Groups["extra"].Value;
31 | if (version_fallback.TryGetValue(Version, out var cv))
32 | ClientVersion = cv;
33 | if (ClientVersion == null)
34 | {
35 | var vcfg = zip.GetEntry("conf/version.cfg");
36 | if (vcfg != null)
37 | {
38 | using var reader = new StreamReader(vcfg.Open());
39 | while (!reader.EndOfStream && ClientVersion == null)
40 | {
41 | var line = reader.ReadLine()!;
42 | var m1 = ClientVersionRegex.Match(line);
43 | if (m1.Success)
44 | ClientVersion = m1.Groups["ver"].Value;
45 | var m2 = ServerVersionRegex.Match(line);
46 | if (m2.Success)
47 | ClientVersion = m2.Groups["ver"].Value;
48 | }
49 | if (ClientVersion != null)
50 | ClientVersion = ClientVersion.Replace("pre", "-pre");
51 | }
52 | if (ClientVersion == null)
53 | {
54 | var readme =
55 | zip.GetEntry("Readme.txt") ??
56 | zip.GetEntry("README-MCP.txt") ??
57 | zip.GetEntry("README-MCP.TXT") ??
58 | zip.GetEntry("docs/README-MCP.TXT");
59 | if (readme != null)
60 | {
61 | using var reader = new StreamReader(readme.Open());
62 | var line = reader.ReadLine()!;
63 | var m = ReadMeRegex.Match(line);
64 | if (m.Success)
65 | {
66 | var ver = m.Groups["ver"].Value;
67 | var series = "a";
68 | if (MajorVersion > 2 || (MajorVersion == 2 && MinorVersion >= 6))
69 | series = "b";
70 | if (!ver.StartsWith(series))
71 | ver = series + ver;
72 | ClientVersion = ver;
73 | }
74 | }
75 | }
76 | }
77 | if (ClientVersion == null)
78 | throw new ArgumentException($"Can't figure out what MC version MCP {Version} is for");
79 |
80 | StreamReader? read(string path)
81 | {
82 | var entry = zip.GetEntry(path);
83 | if (entry == null)
84 | return null;
85 | return new(entry.Open());
86 | }
87 | using (var joined_srg = read("conf/joined.srg"))
88 | {
89 | if (joined_srg != null)
90 | MappingsIO.ParseSrg(LocalMappings.Client, joined_srg);
91 | }
92 | using (var joined_srg = read("conf/joined.srg"))
93 | {
94 | if (joined_srg != null)
95 | MappingsIO.ParseSrg(LocalMappings.Client, joined_srg);
96 | }
97 |
98 | using var client_srg = read("conf/client.srg");
99 | if (client_srg != null)
100 | MappingsIO.ParseSrg(LocalMappings.Client, client_srg);
101 | using var server_srg = read("conf/server.srg");
102 | if (server_srg != null)
103 | MappingsIO.ParseSrg(LocalMappings.Server, server_srg);
104 |
105 | using var client_rgs = read("conf/minecraft.rgs") ?? read(@"conf\minecraft.rgs");
106 | if (client_rgs != null)
107 | ParseRGS(LocalMappings.Client, client_rgs);
108 | using var server_rgs = read("conf/minecraft_server.rgs") ?? read(@"conf\minecraft_server.rgs");
109 | if (server_rgs != null)
110 | ParseRGS(LocalMappings.Server, server_rgs);
111 |
112 | var newids = read("conf/newids.csv");
113 | if (newids != null)
114 | ParseNewIDs(newids);
115 |
116 | ParseCSVs(
117 | classes: read("conf/classes.csv"),
118 | methods: read("conf/methods.csv"),
119 | fields: read("conf/fields.csv")
120 | );
121 | }
122 |
123 | private void ParseNewIDs(StreamReader reader)
124 | {
125 | var ids = ParseCSV(reader).ToList();
126 | foreach (var item in ids.Skip(1))
127 | {
128 | if (item[0] != "*")
129 | NewIDs.Client.Add(item[0], item[2]);
130 | if (item[1] != "*")
131 | NewIDs.Server.Add(item[1], item[2]);
132 | }
133 | }
134 |
135 | private void ParseRGS(Mappings mappings, StreamReader reader)
136 | {
137 | while (!reader.EndOfStream)
138 | {
139 | var line = reader.ReadLine();
140 | if (String.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
141 | continue;
142 | var entries = line.Split(' ');
143 | var type = entries[0];
144 | var name = entries[1];
145 | // fix for a couple random non-obfuscated classes getting renamed for no reason
146 | if (name.StartsWith("com/jcraft") || name.StartsWith("paulscode"))
147 | continue;
148 | if (type == ".class_map")
149 | mappings.AddClass(name.Replace('/', '.'), entries[2].Replace('/', '.'));
150 | if (type == ".class")
151 | mappings.AddClass(name.Replace('/', '.'), name.Replace('/', '.'));
152 | else if (type == ".field_map")
153 | {
154 | var (path, namepart) = MappingsIO.Split(name);
155 | mappings.GetOrAddClass(path).AddField(namepart, entries[2]);
156 | }
157 | else if (type == ".method_map")
158 | {
159 | var (path, namepart) = MappingsIO.Split(name);
160 | mappings.GetOrAddClass(path).AddMethod(namepart, entries[3], entries[2]);
161 | }
162 | }
163 | }
164 | }
165 |
166 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Abstract/Updater.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public abstract class Updater
4 | {
5 | protected VersionGraph Graph;
6 | protected readonly AppConfig Config;
7 | private Dictionary CommitToVersion;
8 | private Dictionary VersionToCommit;
9 |
10 | protected Updater(AppConfig config)
11 | {
12 | Config = config;
13 | CreateGraph();
14 | }
15 |
16 | public void Perform()
17 | {
18 | Console.WriteLine("Graph:");
19 | Console.WriteLine(Graph.ToString());
20 | var versions = Graph.Flatten();
21 | LoadCommits();
22 | foreach (var version in versions)
23 | {
24 | Commit(version);
25 | }
26 | }
27 |
28 | protected void CreateGraph()
29 | {
30 | Profiler.Start("Building version graph");
31 | var versions = FindVersions().Distinct(new DuplicateRemover()).ToList();
32 | var git_versions = VersionConfig.GitRepo.CommittedVersions().Select(x => new GitVersion(x));
33 | versions.AddRange(git_versions.Where(x => !versions.Any(y => x.Name == y.Name)));
34 | Graph = new VersionGraph(VersionConfig.VersionFacts, versions);
35 | Profiler.Stop();
36 | }
37 |
38 | private class DuplicateRemover : IEqualityComparer
39 | {
40 | public bool Equals(Version? x, Version? y)
41 | {
42 | return x.Name == y.Name && x.ReleaseTime == y.ReleaseTime;
43 | }
44 |
45 | public int GetHashCode([DisallowNull] Version obj)
46 | {
47 | return obj.Name.GetHashCode();
48 | }
49 | }
50 |
51 | protected abstract VersionConfig VersionConfig { get; }
52 |
53 | private void LoadCommits()
54 | {
55 | var versions = Graph.Flatten().ToList();
56 | CommitToVersion = new Dictionary();
57 | VersionToCommit = new Dictionary();
58 | Profiler.Start("Loading commits");
59 | var commits = VersionConfig.GitRepo.CommittedVersions().ToList();
60 | foreach (var entry in commits)
61 | {
62 | var version = versions.FirstOrDefault(x => x.Version.Name == entry.Message);
63 | if (version != null)
64 | {
65 | CommitToVersion[entry.Hash] = version;
66 | VersionToCommit[version] = entry.Hash;
67 | }
68 | }
69 |
70 | Profiler.Stop();
71 | }
72 |
73 | private void Commit(VersionNode version)
74 | {
75 | if (VersionToCommit.ContainsKey(version))
76 | return;
77 | if (version.Parent == null)
78 | {
79 | Console.WriteLine($"{version.Version.Name} is the first version in the history!");
80 | InitialCommit(version);
81 | }
82 | else
83 | {
84 | Console.WriteLine($"Parent of {version.Version.Name} is {version.Parent.Version.Name}");
85 | if (!VersionToCommit.ContainsKey(version.Parent))
86 | {
87 | Console.WriteLine("Need to commit parent first");
88 | Commit(version.Parent);
89 | }
90 |
91 | InsertCommit(version);
92 | }
93 | }
94 |
95 | private string GetBranchName(VersionNode version) => version.ReleaseName.Replace(' ', '-');
96 |
97 | private void InitialCommit(VersionNode version)
98 | {
99 | Profiler.Start("Initializing repo");
100 | VersionConfig.GitRepo.Init();
101 | if (Config.GitIgnoreContents != null)
102 | File.WriteAllText(Path.Combine(VersionConfig.GitRepo.Folder, ".gitignore"), Config.GitIgnoreContents);
103 | // create branch
104 | VersionConfig.GitRepo.CheckoutBranch(GetBranchName(version));
105 | Profiler.Stop();
106 | DoCommit(version);
107 | }
108 |
109 | private void InsertCommit(VersionNode version)
110 | {
111 | // find commit hash for existing version
112 | string hash = VersionToCommit[version.Parent];
113 | // create branch
114 | var branchname = GetBranchName(version);
115 | VersionConfig.GitRepo.MakeBranch(branchname, hash);
116 | // if this commit is the most recent for this branch, we can just commit right on top without insertion logic
117 | string tophash = VersionConfig.GitRepo.BranchHash(branchname);
118 | if (hash == tophash)
119 | {
120 | VersionConfig.GitRepo.Checkout(branchname);
121 | DoCommit(version);
122 | }
123 | else
124 | {
125 | Console.WriteLine($"Needs to insert into history for this one");
126 | // make a branch that starts there and prepare to commit to it
127 | VersionConfig.GitRepo.CheckoutBranch("temp", hash);
128 | VersionConfig.GitRepo.MakeBranch(branchname);
129 | DoCommit(version);
130 | // insert
131 | Profiler.Start($"Rebasing");
132 | VersionConfig.GitRepo.Rebase("temp", branchname);
133 | VersionConfig.GitRepo.Checkout(branchname);
134 | VersionConfig.GitRepo.DeleteBranch("temp");
135 | Profiler.Stop();
136 | // need to rescan since commit hashes change after a rebase
137 | LoadCommits();
138 | }
139 | }
140 |
141 | private void DoCommit(VersionNode version)
142 | {
143 | Profiler.Start($"Adding commit for {version.Version}");
144 | // extract
145 | string workspace = Path.Combine(Path.GetTempPath(), "mc_version_history_workspace");
146 | if (Directory.Exists(workspace))
147 | Directory.Delete(workspace, true);
148 | Directory.CreateDirectory(workspace);
149 | Profiler.Run("Extracting", () => { version.Version.ExtractData(workspace, Config); });
150 | Profiler.Run("Unzipping", () => { UnzipZips(workspace); });
151 | Profiler.Run("Translating NBT Files", () => { TranslateNbtFiles(workspace); });
152 | Profiler.Run($"Merging", () =>
153 | {
154 | MergeWithWorkspace(VersionConfig.GitRepo.Folder, workspace);
155 | Directory.Delete(workspace, true);
156 | Util.RemoveEmptyFolders(VersionConfig.GitRepo.Folder);
157 | File.WriteAllText(Path.Combine(VersionConfig.GitRepo.Folder, "version.txt"), version.Version.Name);
158 | });
159 | // commit
160 | Profiler.Start($"Running git commit");
161 | VersionConfig.GitRepo.Commit(version.Version.Name, version.Version.ReleaseTime);
162 | string hash = VersionConfig.GitRepo.BranchHash("HEAD");
163 | CommitToVersion.Add(hash, version);
164 | VersionToCommit.Add(version, hash);
165 | Profiler.Stop(); // git commit
166 | Profiler.Stop(); // top commit
167 | }
168 |
169 | // read all NBT files in the version and write textual copies
170 | private void TranslateNbtFiles(string root_folder)
171 | {
172 | // don't enumerate because we're creating new files as we go
173 | foreach (var path in Directory.GetFiles(root_folder, "*", SearchOption.AllDirectories))
174 | {
175 | VersionConfig.TranslateNbt(path);
176 | }
177 | }
178 |
179 | private void UnzipZips(string root_folder)
180 | {
181 | foreach (var path in Directory.GetFiles(root_folder, "*.zip", SearchOption.AllDirectories))
182 | {
183 | ZipFile.ExtractToDirectory(path, path + "_dir");
184 | File.Delete(path);
185 | Directory.Move(path + "_dir", path);
186 | }
187 | }
188 |
189 | private void MergeWithWorkspace(string base_folder, string workspace)
190 | {
191 | // delete files that are not present in workspace
192 | Profiler.Start("Deleting");
193 | foreach (var item in Directory.GetFiles(base_folder, "*", SearchOption.AllDirectories))
194 | {
195 | string relative = Path.GetRelativePath(base_folder, item);
196 | if (relative.StartsWith(".git"))
197 | continue;
198 | string workspace_version = Path.Combine(workspace, relative);
199 | if (!File.Exists(workspace_version))
200 | File.Delete(item);
201 | }
202 |
203 | Profiler.Stop();
204 |
205 | // copy new/changed files from workspace
206 | Profiler.Start("Copying");
207 | foreach (var item in Directory.GetFiles(workspace, "*", SearchOption.AllDirectories))
208 | {
209 | string relative = Path.GetRelativePath(workspace, item);
210 | string base_version = Path.Combine(base_folder, relative);
211 | if (!File.Exists(base_version) || !Util.FilesAreEqual(new FileInfo(item), new FileInfo(base_version)))
212 | Util.Copy(item, base_version);
213 | File.Delete(item);
214 | }
215 |
216 | Profiler.Stop();
217 | }
218 |
219 | protected abstract IEnumerable FindVersions();
220 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/VersionedRenames.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class VersionedRenames
4 | {
5 | private readonly List<(VersionSpec spec, Sided renames)> Renames;
6 | public VersionedRenames(YamlMappingNode node)
7 | {
8 | Renames = ((YamlSequenceNode)node.Go("mappings")).OfType().Select(ParseNode).ToList();
9 | }
10 |
11 | public VersionedRenames()
12 | {
13 | Renames = new();
14 | }
15 |
16 | private string GetSidedItem(string version, IEnumerable equiv, Func, string, string> getter)
17 | {
18 | foreach (var (spec, renames) in Renames.Where(x => x.spec.AppliesTo(version)))
19 | {
20 | foreach (var item in equiv)
21 | {
22 | var result = getter(renames, item);
23 | if (result != null)
24 | return result;
25 | }
26 | }
27 | return null;
28 | }
29 |
30 | public string GetClientClass(string version, string name, Equivalencies eq)
31 | {
32 | return GetSidedItem(version, eq.GetEquivalentClasses(name), (x, y) => x.Client.GetClass(y));
33 | }
34 |
35 | public string GetServerClass(string version, string name, Equivalencies eq)
36 | {
37 | return GetSidedItem(version, eq.GetEquivalentClasses(name), (x, y) => x.Server.GetClass(y));
38 | }
39 |
40 | public string GetClientMethod(string version, string name, Equivalencies eq)
41 | {
42 | return GetSidedItem(version, eq.GetEquivalentMethods(name), (x, y) => x.Client.GetMethod(y));
43 | }
44 |
45 | public string GetServerMethod(string version, string name, Equivalencies eq)
46 | {
47 | return GetSidedItem(version, eq.GetEquivalentMethods(name), (x, y) => x.Server.GetMethod(y));
48 | }
49 |
50 | public string GetClientField(string version, string name, Equivalencies eq)
51 | {
52 | return GetSidedItem(version, eq.GetEquivalentFields(name), (x, y) => x.Client.GetField(y));
53 | }
54 |
55 | public string GetServerField(string version, string name, Equivalencies eq)
56 | {
57 | return GetSidedItem(version, eq.GetEquivalentFields(name), (x, y) => x.Server.GetField(y));
58 | }
59 |
60 | public void Add(VersionSpec spec, Sided renames)
61 | {
62 | Renames.Add((spec, renames));
63 | }
64 |
65 | private (VersionSpec spec, Sided renames) ParseNode(YamlMappingNode node)
66 | {
67 | VersionSpec spec = new(node.Go("affects"));
68 | Sided renames = new();
69 | void add_flat(string side, Action adder)
70 | {
71 | if (side == "client" || side == "joined")
72 | adder(renames.Client);
73 | if (side == "server" || side == "joined")
74 | adder(renames.Server);
75 | }
76 | foreach (var side in new[] { "client", "server", "joined" })
77 | {
78 | var class_renames = node.Go("mappings", side, "classes").ToDictionary() ?? new();
79 | var field_renames = node.Go("mappings", side, "fields").ToDictionary() ?? new();
80 | var method_renames = node.Go("mappings", side, "methods").ToDictionary() ?? new();
81 | foreach (var c in class_renames)
82 | {
83 | add_flat(side, x => x.AddClass(c.Key, c.Value));
84 | }
85 | foreach (var f in field_renames)
86 | {
87 | add_flat(side, x => x.AddField(f.Key, f.Value));
88 | }
89 | foreach (var m in method_renames)
90 | {
91 | add_flat(side, x => x.AddMethod(m.Key, m.Value));
92 | }
93 | }
94 | return (spec, renames);
95 | }
96 |
97 | private static (FlatMap client, FlatMap server, FlatMap joined) Split(Sided map)
98 | {
99 | var client = new FlatMap();
100 | var server = new FlatMap();
101 | var joined = new FlatMap();
102 | void send(Func> getter, Action adder)
103 | {
104 | var client_items = getter(map.Client).ToHashSet();
105 | var server_items = getter(map.Server).ToHashSet();
106 | foreach (var item in client_items.Intersect(server_items))
107 | {
108 | adder(joined, item.OldName, item.NewName);
109 | }
110 | foreach (var item in client_items.Except(server_items))
111 | {
112 | adder(client, item.OldName, item.NewName);
113 | }
114 | foreach (var item in server_items.Except(client_items))
115 | {
116 | adder(server, item.OldName, item.NewName);
117 | }
118 | }
119 | send(x => x.ClassMap, (x, y, z) => x.AddClass(y, z));
120 | send(x => x.MethodMap, (x, y, z) => x.AddMethod(y, z));
121 | send(x => x.FieldMap, (x, y, z) => x.AddField(y, z));
122 | return (client, server, joined);
123 | }
124 |
125 | private static void AddIfPresent(YamlMappingNode node, string key, YamlNode value)
126 | {
127 | if (value is YamlScalarNode || (value is YamlSequenceNode seq && seq.Children.Count > 0) || (value is YamlMappingNode map && map.Children.Count > 0))
128 | node.Add(key, value);
129 | }
130 |
131 | public void WriteTo(string file)
132 | {
133 | var root = new YamlMappingNode();
134 | var list = new YamlSequenceNode();
135 | foreach (var (spec, renames) in Renames)
136 | {
137 | var node = new YamlMappingNode();
138 | node.Add("affects", spec.Serialize());
139 | list.Add(node);
140 | var mappings = new YamlMappingNode();
141 | var (client, server, joined) = Split(renames);
142 | var sides = new[] { ("client", client), ("server", server), ("joined", joined) };
143 | foreach (var (name, map) in sides)
144 | {
145 | var side_node = new YamlMappingNode();
146 | AddIfPresent(side_node, "classes", SerializeMappings(map.ClassMap));
147 | AddIfPresent(side_node, "fields", SerializeMappings(map.FieldMap));
148 | AddIfPresent(side_node, "methods", SerializeMappings(map.MethodMap));
149 | AddIfPresent(mappings, name, side_node);
150 | }
151 | AddIfPresent(node, "mappings", mappings);
152 | }
153 | AddIfPresent(root, "mappings", list);
154 | YamlHelper.SaveToFile(root, file);
155 | }
156 |
157 | private class RenameSorter : IComparer
158 | {
159 | public int Compare(string? x, string? y)
160 | {
161 | string[] x_scores = x.Split('_');
162 | string[] y_scores = y.Split('_');
163 | for (int i = 0; i < Math.Min(x_scores.Length, y_scores.Length); i++)
164 | {
165 | if (int.TryParse(x_scores[i], out int xi) && int.TryParse(y_scores[i], out int yi))
166 | {
167 | int compare = xi.CompareTo(yi);
168 | if (compare != 0)
169 | return compare;
170 | }
171 | int compare_str = x_scores[i].CompareTo(y_scores[i]);
172 | if (compare_str != 0)
173 | return compare_str;
174 | }
175 | return x_scores.Length.CompareTo(y_scores.Length);
176 | }
177 | }
178 |
179 | private YamlMappingNode SerializeMappings(IEnumerable renames)
180 | {
181 | var node = new YamlMappingNode();
182 | foreach (var item in renames.OrderBy(x => x.OldName, new RenameSorter()))
183 | {
184 | node.Add(item.OldName, item.NewName);
185 | }
186 | return node;
187 | }
188 | }
189 |
190 | public class VersionSpec
191 | {
192 | private readonly bool AllVersions = false;
193 | private readonly List Accepted = new();
194 |
195 | public VersionSpec(string version)
196 | {
197 | Accepted.Add(version);
198 | }
199 |
200 | public VersionSpec(IEnumerable versions)
201 | {
202 | Accepted.AddRange(versions);
203 | }
204 |
205 | private VersionSpec(bool all)
206 | {
207 | AllVersions = all;
208 | }
209 |
210 | public static VersionSpec All => new VersionSpec(true);
211 |
212 | public VersionSpec(YamlNode node)
213 | {
214 | if (node is YamlScalarNode n)
215 | {
216 | string val = n.Value;
217 | if (val == "*")
218 | AllVersions = true;
219 | else
220 | Accepted.Add(val);
221 | }
222 | else if (node is YamlSequenceNode s)
223 | Accepted.AddRange(s.ToStringList());
224 | }
225 |
226 | public YamlNode Serialize()
227 | {
228 | if (AllVersions)
229 | return new YamlScalarNode("*");
230 | if (Accepted.Count == 1)
231 | return new YamlScalarNode(Accepted[0]);
232 | return new YamlSequenceNode(Accepted.Select(x => new YamlScalarNode(x)));
233 | }
234 |
235 | public bool AppliesTo(string version)
236 | {
237 | return AllVersions || Accepted.Contains(version);
238 | }
239 | }
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/RetroMCP.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class RetroMCP
4 | {
5 | public readonly string Folder;
6 | private readonly Sided MatchedMCP;
7 | private readonly Sided MatchedMojang;
8 | private readonly VersionedRenames FoundRenames;
9 | private readonly VersionedRenames CustomRenames;
10 | private readonly Sided MergedEquivalencies;
11 | public RetroMCP(string folder, string matched_version)
12 | {
13 | Folder = folder;
14 | var mcp = ParseTsrgs(matched_version);
15 | MatchedMCP = new(mcp.Client.Reversed(), mcp.Server.Reversed());
16 | MatchedMojang = new();
17 | using var client_file = File.OpenText(Path.Combine(Folder, "matched_client.txt"));
18 | using var server_file = File.OpenText(Path.Combine(Folder, "matched_server.txt"));
19 | MappingsIO.ParseProguard(MatchedMojang.Client, client_file);
20 | MappingsIO.ParseProguard(MatchedMojang.Server, server_file);
21 | FoundRenames = new((YamlMappingNode)YamlHelper.ParseFile(Path.Combine(folder, "mappings_found.yaml")));
22 | CustomRenames = new((YamlMappingNode)YamlHelper.ParseFile(Path.Combine(folder, "mappings_custom.yaml")));
23 | var found_equivs = Equivalencies.Parse((YamlMappingNode)YamlHelper.ParseFile(Path.Combine(folder, "equivalencies_custom.yaml")));
24 | var custom_equivs = Equivalencies.Parse((YamlMappingNode)YamlHelper.ParseFile(Path.Combine(folder, "equivalencies_custom.yaml")));
25 | var client_equivs = new Equivalencies(found_equivs.Client, custom_equivs.Client);
26 | var server_equivs = new Equivalencies(found_equivs.Server, custom_equivs.Server);
27 | MergedEquivalencies = new(client_equivs, server_equivs);
28 | }
29 |
30 | private Sided ParseTsrgs(string version)
31 | {
32 | var sided = new Sided();
33 | using var client_file = File.OpenText(Path.Combine(Folder, version, "client.tsrg"));
34 | using var server_file = File.OpenText(Path.Combine(Folder, version, "server.tsrg"));
35 | MappingsIO.ParseTsrg(sided.Client, client_file);
36 | MappingsIO.ParseTsrg(sided.Server, server_file);
37 | return sided;
38 | }
39 |
40 | public Sided CreateMappings(string version)
41 | {
42 | if (!Directory.Exists(Path.Combine(Folder, version)))
43 | return null;
44 | var final = new Sided();
45 | var local = ParseTsrgs(version);
46 | var sides = new (
47 | Func, Mappings> map,
48 | Func, FlatMap> flat,
49 | Equivalencies eq,
50 | Func> class_getter,
51 | Func> method_getter,
52 | Func> field_getter
53 | )[]
54 | {
55 | (x => x.Client, x => x.Client, MergedEquivalencies.Client, x=>(a,b,c)=>x.GetClientClass(a,b,c), x=>(a,b,c)=>x.GetClientMethod(a,b,c), x=>(a,b,c)=>x.GetClientField(a,b,c)),
56 | (x => x.Server, x => x.Server, MergedEquivalencies.Server, x=>(a,b,c)=>x.GetServerClass(a,b,c), x=>(a,b,c)=>x.GetServerMethod(a,b,c), x=>(a,b,c)=>x.GetServerField(a,b,c))
57 | };
58 | foreach (var (map, flat, eq, class_getter, method_getter, field_getter) in sides)
59 | {
60 | foreach (var c in map(local).ClassList)
61 | {
62 | var matched_mcp = map(MatchedMCP).GetClass(c.NewName, eq);
63 | MappedClass matched_mojang()
64 | {
65 | if (matched_mcp == null)
66 | return null;
67 | return map(MatchedMojang).GetClass(matched_mcp.NewName, eq);
68 | }
69 | MappedClass mojang = matched_mojang();
70 | static void WriteText(string text, ConsoleColor color)
71 | {
72 | Console.ForegroundColor = color;
73 | Console.WriteLine(text);
74 | Console.ResetColor();
75 | }
76 | MappedClass find_mojang()
77 | {
78 | if (mojang == null)
79 | return null;
80 | WriteText($"Class {c.OldName}: Mojang Match -> {mojang.NewName}", ConsoleColor.Green);
81 | return map(final).AddClass(c.OldName, mojang.NewName);
82 | }
83 | MappedClass find_custom(VersionedRenames rename)
84 | {
85 | string new_name = class_getter(rename)(version, c.NewName, eq);
86 | if (new_name == null)
87 | return null;
88 | WriteText($"Class {c.OldName}: Rename Match -> {new_name}", rename == CustomRenames ? ConsoleColor.Cyan : ConsoleColor.Yellow);
89 | return map(final).AddClass(c.OldName, new_name);
90 | }
91 | MappedClass give_up()
92 | {
93 | WriteText($"Class {c.OldName}: No Match -> {c.NewName}", ConsoleColor.Red);
94 | return map(final).AddClass(c.OldName, c.NewName);
95 | }
96 | MappedClass final_class = find_mojang() ?? find_custom(CustomRenames) ?? find_custom(FoundRenames) ?? give_up();
97 | MappedField find_mojang_field(MappedField field)
98 | {
99 | if (mojang == null)
100 | return null;
101 | var mcp_field = matched_mcp.GetField(field.NewName, eq);
102 | if (mcp_field == null)
103 | return null;
104 | var matched_field = mojang.GetField(mcp_field.NewName, eq);
105 | if (matched_field == null)
106 | return null;
107 | WriteText($"\tField {field.OldName}: Mojang Match -> {matched_field.NewName}", ConsoleColor.Green);
108 | return final_class.AddField(field.OldName, matched_field.NewName);
109 | }
110 | MappedField find_custom_field(MappedField field, VersionedRenames rename)
111 | {
112 | string new_name = field_getter(rename)(version, field.NewName, eq);
113 | if (new_name == null)
114 | return null;
115 | WriteText($"\tField {field.OldName}: Custom Match -> {new_name}", rename == CustomRenames ? ConsoleColor.Cyan : ConsoleColor.Yellow);
116 | return final_class.AddField(field.OldName, new_name);
117 | }
118 | MappedField give_up_field(MappedField field)
119 | {
120 | WriteText($"\tField {field.OldName}: No Match -> {field.NewName}", ConsoleColor.Red);
121 | return final_class.AddField(field.OldName, field.NewName);
122 | }
123 | MappedMethod find_mojang_method(MappedMethod method)
124 | {
125 | if (mojang == null)
126 | return null;
127 | var mcp_method = matched_mcp.GetMethod(method.NewName, method.Signature, eq);
128 | if (mcp_method == null)
129 | return null;
130 | var matched_method = mojang.GetMethod(mcp_method.NewName, method.Signature, eq);
131 | if (matched_method == null)
132 | return null;
133 | WriteText($"\tMethod {method.OldName}: Mojang Match -> {matched_method.NewName}", ConsoleColor.Green);
134 | return final_class.AddMethod(method.OldName, matched_method.NewName, method.Signature);
135 | }
136 | MappedMethod find_custom_method(MappedMethod method, VersionedRenames rename)
137 | {
138 | string new_name = method_getter(rename)(version, method.NewName, eq);
139 | if (new_name == null)
140 | return null;
141 | WriteText($"\tMethod {method.OldName}: Custom Match -> {new_name}", rename == CustomRenames ? ConsoleColor.Cyan : ConsoleColor.Yellow);
142 | return final_class.AddMethod(method.OldName, new_name, method.Signature);
143 | }
144 | MappedMethod give_up_method(MappedMethod method)
145 | {
146 | WriteText($"\tMethod {method.OldName}: No Match -> {method.NewName}", ConsoleColor.Red);
147 | return final_class.AddMethod(method.OldName, method.NewName, method.Signature);
148 | }
149 | foreach (var item in c.FieldList)
150 | {
151 | MappedField final_field = find_mojang_field(item) ?? find_custom_field(item, CustomRenames) ?? find_custom_field(item, FoundRenames) ?? give_up_field(item);
152 | }
153 | foreach (var item in c.MethodList)
154 | {
155 | MappedMethod final_method = find_mojang_method(item) ?? find_custom_method(item, CustomRenames) ?? find_custom_method(item, FoundRenames) ?? give_up_method(item);
156 | }
157 | }
158 | }
159 | return final;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/MCP/Mappings/Equivalencies.cs:
--------------------------------------------------------------------------------
1 | namespace MinecraftVersionHistory;
2 |
3 | public class Equivalencies
4 | {
5 | private readonly List> EquivalentClasses;
6 | private readonly List> EquivalentFields;
7 | private readonly List> EquivalentMethods;
8 | public IEnumerable> Classes => EquivalentClasses;
9 | public IEnumerable> Fields => EquivalentFields;
10 | public IEnumerable> Methods => EquivalentMethods;
11 |
12 | public static Sided Parse(YamlMappingNode node)
13 | {
14 | var eq = new Sided();
15 | foreach (var side in new[] { "client", "server", "joined" })
16 | {
17 | var classes = node.Go("equivalencies", side, "classes").NullableParse(x => ParseEquivalencies((YamlSequenceNode)x)) ?? new();
18 | var fields = node.Go("equivalencies", side, "fields").NullableParse(x => ParseEquivalencies((YamlSequenceNode)x)) ?? new();
19 | var methods = node.Go("equivalencies", side, "methods").NullableParse(x => ParseEquivalencies((YamlSequenceNode)x)) ?? new();
20 | if (side == "client" || side == "joined")
21 | {
22 | foreach (var item in classes)
23 | {
24 | eq.Client.AddClasses(item);
25 | }
26 | foreach (var item in fields)
27 | {
28 | eq.Client.AddFields(item);
29 | }
30 | foreach (var item in methods)
31 | {
32 | eq.Client.AddMethods(item);
33 | }
34 | }
35 | if (side == "server" || side == "joined")
36 | {
37 | foreach (var item in classes)
38 | {
39 | eq.Server.AddClasses(item);
40 | }
41 | foreach (var item in fields)
42 | {
43 | eq.Server.AddFields(item);
44 | }
45 | foreach (var item in methods)
46 | {
47 | eq.Server.AddMethods(item);
48 | }
49 | }
50 | }
51 | return eq;
52 | }
53 |
54 | public static void WriteTo(Sided equivs, string file)
55 | {
56 | var root = new YamlMappingNode();
57 | var equiv_node = new YamlMappingNode();
58 | var (eclient, eserver, ejoined) = Split(equivs);
59 | var esides = new[] { ("client", eclient), ("server", eserver), ("joined", ejoined) };
60 | foreach (var (name, eq) in esides)
61 | {
62 | AddIfPresent(equiv_node, name, SerializeEquivalencies(eq));
63 | }
64 | AddIfPresent(root, "equivalencies", equiv_node);
65 | YamlHelper.SaveToFile(root, file);
66 | }
67 |
68 | private static YamlMappingNode SerializeEquivalencies(Equivalencies eq)
69 | {
70 | var node = new YamlMappingNode();
71 | var classes = new YamlSequenceNode();
72 | var methods = new YamlSequenceNode();
73 | var fields = new YamlSequenceNode();
74 | foreach (var item in eq.Classes)
75 | {
76 | var sub = new YamlSequenceNode();
77 | foreach (var i in item)
78 | {
79 | sub.Add(i);
80 | }
81 | classes.Add(sub);
82 | }
83 | foreach (var item in eq.Methods)
84 | {
85 | var sub = new YamlSequenceNode();
86 | foreach (var i in item)
87 | {
88 | sub.Add(i);
89 | }
90 | methods.Add(sub);
91 | }
92 | foreach (var item in eq.Fields)
93 | {
94 | var sub = new YamlSequenceNode();
95 | foreach (var i in item)
96 | {
97 | sub.Add(i);
98 | }
99 | fields.Add(sub);
100 | }
101 | AddIfPresent(node, "classes", classes);
102 | AddIfPresent(node, "methods", methods);
103 | AddIfPresent(node, "fields", fields);
104 | return node;
105 | }
106 |
107 | private static void AddIfPresent(YamlMappingNode node, string key, YamlNode value)
108 | {
109 | if (value is YamlScalarNode || (value is YamlSequenceNode seq && seq.Children.Count > 0) || (value is YamlMappingNode map && map.Children.Count > 0))
110 | node.Add(key, value);
111 | }
112 |
113 | private static (Equivalencies client, Equivalencies server, Equivalencies joined) Split(Sided sided)
114 | {
115 | var client = new Equivalencies();
116 | var server = new Equivalencies();
117 | var joined = new Equivalencies();
118 | void send(Func>> getter, Action> adder)
119 | {
120 | var client_items = getter(sided.Client).Select(x => x.ToHashSet()).ToList();
121 | var server_items = getter(sided.Server).Select(x => x.ToHashSet()).ToList();
122 | var comparer = HashSet.CreateSetComparer();
123 | foreach (var item in client_items.Intersect(server_items, comparer))
124 | {
125 | adder(joined, item);
126 | }
127 | foreach (var item in client_items.Except(server_items, comparer))
128 | {
129 | adder(client, item);
130 | }
131 | foreach (var item in server_items.Except(client_items, comparer))
132 | {
133 | adder(server, item);
134 | }
135 | }
136 | send(x => x.Classes, (x, y) => x.AddClasses(y));
137 | send(x => x.Methods, (x, y) => x.AddMethods(y));
138 | send(x => x.Fields, (x, y) => x.AddFields(y));
139 | return (client, server, joined);
140 | }
141 |
142 | private static List> ParseEquivalencies(YamlSequenceNode node)
143 | {
144 | var list = new List>();
145 | foreach (var item in node)
146 | {
147 | if (item is YamlSequenceNode seq)
148 | {
149 | var set = new HashSet(seq.Select(x => x.String()));
150 | list.Add(set);
151 | }
152 | else if (item is YamlMappingNode map)
153 | {
154 | foreach (var sub in map.Children)
155 | {
156 | list.Add(new() { sub.Key.String(), sub.Value.String() });
157 | }
158 | }
159 | }
160 | return list;
161 | }
162 |
163 | public Equivalencies(params Equivalencies[] merge_in) : this()
164 | {
165 | foreach (var item in merge_in)
166 | {
167 | foreach (var i in item.Classes)
168 | {
169 | AddClasses(i);
170 | }
171 | foreach (var i in item.Methods)
172 | {
173 | AddMethods(i);
174 | }
175 | foreach (var i in item.Fields)
176 | {
177 | AddFields(i);
178 | }
179 | }
180 | }
181 |
182 | public Equivalencies()
183 | {
184 | EquivalentClasses = new();
185 | EquivalentFields = new();
186 | EquivalentMethods = new();
187 | }
188 |
189 | public Equivalencies(IEnumerable> classes, IEnumerable> fields, IEnumerable> methods)
190 | {
191 | EquivalentClasses = classes.Select(x => new HashSet(x)).ToList();
192 | EquivalentFields = fields.Select(x => new HashSet(x)).ToList();
193 | EquivalentMethods = methods.Select(x => new HashSet(x)).ToList();
194 | }
195 |
196 | private IEnumerable GetEquivalencies(List> list, string name)
197 | {
198 | return (IEnumerable)FindSet(list, name) ?? new[] { name };
199 | }
200 |
201 | public IEnumerable GetEquivalentClasses(string name)
202 | {
203 | return GetEquivalencies(EquivalentClasses, name);
204 | }
205 |
206 | public IEnumerable GetEquivalentMethods(string name)
207 | {
208 | return GetEquivalencies(EquivalentMethods, name);
209 | }
210 |
211 | public IEnumerable GetEquivalentFields(string name)
212 | {
213 | return GetEquivalencies(EquivalentFields, name);
214 | }
215 |
216 | public void AddClasses(IEnumerable classes)
217 | {
218 | AddEquivalencies(EquivalentClasses, classes);
219 | }
220 |
221 | public void AddMethods(IEnumerable methods)
222 | {
223 | AddEquivalencies(EquivalentMethods, methods);
224 | }
225 |
226 | public void AddFields(IEnumerable fields)
227 | {
228 | AddEquivalencies(EquivalentFields, fields);
229 | }
230 |
231 | private HashSet FindOrCreateSet(List> list, string entry)
232 | {
233 | var set = FindSet(list, entry);
234 | if (set != null)
235 | return set;
236 | var new_set = new HashSet();
237 | list.Add(new_set);
238 | return new_set;
239 | }
240 |
241 | private HashSet FindSet(List> list, string entry)
242 | {
243 | foreach (var set in list)
244 | {
245 | if (set.Contains(entry))
246 | return set;
247 | }
248 | return null;
249 | }
250 |
251 | private void AddEquivalencies(List> list, IEnumerable items)
252 | {
253 | if (!items.Any())
254 | return;
255 | var set = FindOrCreateSet(list, items.First());
256 | set.UnionWith(items);
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/MinecraftVersionHistory/Java/JavaVersion.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Threading.Tasks;
3 | using JetBrains.Annotations;
4 | using Microsoft.VisualBasic.CompilerServices;
5 |
6 | namespace MinecraftVersionHistory;
7 |
8 | public class JavaVersion : Version
9 | {
10 | public EndpointData Server { get; private set; }
11 | public EndpointData Client { get; private set; }
12 | public readonly string AssetsURL;
13 | public readonly string ServerJarURL;
14 | public readonly string JarFilePath;
15 | public readonly string LauncherJsonPath;
16 |
17 | public JavaVersion(string folder, VersionFacts facts)
18 | {
19 | Name = Path.GetFileName(folder);
20 | Name = facts.CustomName(Name) ?? Name;
21 | JarFilePath = Path.Combine(folder, Name + ".jar");
22 | LauncherJsonPath = Path.Combine(folder, Name + ".json");
23 | var json = JsonObject.Parse(File.ReadAllText(LauncherJsonPath));
24 | ReleaseTime = DateTime.Parse(json["releaseTime"].ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None);
25 | Client = new(
26 | "client",
27 | Path.Combine(folder, Name + ".jar"),
28 | (string)json["downloads"]?["client_mappings"]?["url"]
29 | );
30 | Server = new(
31 | "server",
32 | null,
33 | (string)json["downloads"]?["server_mappings"]?["url"]
34 | );
35 | ServerJarURL = (string)json["downloads"]?["server"]?["url"];
36 | AssetsURL = (string)json["assetIndex"]?["url"];
37 | }
38 |
39 | public static bool LooksValid(string folder)
40 | {
41 | string name = Path.GetFileName(folder);
42 | string jsonpath = Path.Combine(folder, name + ".json");
43 | string jarpath = Path.Combine(folder, name + ".jar");
44 | return File.Exists(jsonpath) && File.Exists(jarpath);
45 | }
46 |
47 | private void RunDataGenerators(JavaConfig config, string folder)
48 | {
49 | string reports_path = Path.Combine(config.ServerJarFolder, "generated");
50 | if (Directory.Exists(reports_path))
51 | Directory.Delete(reports_path, true);
52 |
53 | DownloadServerJar(config);
54 | if (Server.JarPath is not null)
55 | {
56 | Profiler.Start("Fetching data reports");
57 | string args1 = $"-cp \"{Server.JarPath}\" net.minecraft.data.Main --reports";
58 | string args2 = $"-DbundlerMainClass=net.minecraft.data.Main -jar \"{Server.JarPath}\" --reports";
59 | var result = CommandRunner.RunJavaCombos(
60 | config.ServerJarFolder,
61 | config.JavaInstallationPaths,
62 | new[] { args1, args2 }
63 | );
64 | if (result.ExitCode != 0)
65 | throw new ApplicationException("Failed to get data reports");
66 | Directory.CreateDirectory(folder);
67 | FileSystem.CopyDirectory(Path.Combine(reports_path, "reports"), folder);
68 | if (Directory.Exists(reports_path))
69 | Directory.Delete(reports_path, true);
70 | Profiler.Stop();
71 | }
72 | }
73 |
74 | private void ExtractJar(JavaConfig config, string folder)
75 | {
76 | Profiler.Start($"Extracting jar");
77 | using ZipArchive zip = ZipFile.OpenRead(Client.JarPath);
78 | foreach (var entry in zip.Entries)
79 | {
80 | if (entry.FullName.EndsWith("/") || config.ExcludeJarEntry(entry.FullName))
81 | continue;
82 | Directory.CreateDirectory(Path.Combine(folder, Path.GetDirectoryName(entry.FullName)));
83 | var destination = Path.Combine(folder, entry.FullName);
84 | entry.ExtractToFile(destination);
85 | }
86 |
87 | Profiler.Stop();
88 | }
89 |
90 | private void FetchAssets(JavaConfig config, string json_path, string folder)
91 | {
92 | Profiler.Start("Fetching assets");
93 | var assets_file = Util.DownloadString(AssetsURL);
94 | File.WriteAllText(json_path, assets_file);
95 | var json = JsonObject.Parse(assets_file);
96 | foreach ((string path, JsonNode data) in (JsonObject)json["objects"])
97 | {
98 | var hash = (string)data["hash"];
99 | var cached = Path.Combine(config.AssetsFolder, "objects", hash[0..2], hash);
100 | var destination = Path.Combine(folder, path);
101 | Directory.CreateDirectory(Path.GetDirectoryName(destination));
102 | if (File.Exists(cached))
103 | File.Copy(cached, destination, true);
104 | else
105 | Util.DownloadFile($"https://resources.download.minecraft.net/{hash[0..2]}/{hash}", destination);
106 | }
107 |
108 | Profiler.Stop();
109 | }
110 |
111 | public override void ExtractData(string folder, AppConfig config)
112 | {
113 | var java_config = config.Java;
114 | if (ReleaseTime > java_config.DataGenerators)
115 | RunDataGenerators(java_config, Path.Combine(folder, "reports"));
116 | if (AssetsURL != null)
117 | FetchAssets(java_config, Path.Combine(folder, "assets.json"), Path.Combine(folder, "assets"));
118 | ExtractJar(java_config, Path.Combine(folder, "jar"));
119 | DecompileMinecraft(java_config, Path.Combine(folder, "source"));
120 | java_config.JsonSort(folder, this);
121 | DownloadPatchNotes(Path.Combine(folder, "patchnotes"));
122 | File.Copy(LauncherJsonPath, Path.Combine(folder, "launcher.json"));
123 | }
124 |
125 | public record EndpointData(string Name, string JarPath, string MappingsURL);
126 |
127 | private string MapJar(JavaConfig config, EndpointData side, string folder)
128 | {
129 | string mappings_path = Path.Combine(Path.GetDirectoryName(folder), $"mappings_{side.Name}.txt");
130 | if (side.MappingsURL != null)
131 | Util.DownloadFile(side.MappingsURL, mappings_path);
132 | else
133 | {
134 | var mcp = config.GetMCPMappings(this);
135 | if (mcp == null)
136 | return null;
137 | Profiler.Start("Using MCP mappings");
138 | using (var writer = new StreamWriter(mappings_path))
139 | {
140 | if (side.Name == "server")
141 | MappingsIO.WriteTsrg(mcp.Server, writer);
142 | else if (side.Name == "client")
143 | MappingsIO.WriteTsrg(mcp.Client, writer);
144 | else
145 | mcp = null;
146 | }
147 |
148 | Profiler.Stop();
149 | if (mcp == null)
150 | return null;
151 | }
152 |
153 | string mapped_jar_path = Path.Combine(folder, $"mapped_{side.Name}.jar");
154 | config.RemapJar(side.JarPath, mappings_path, mapped_jar_path);
155 | return mapped_jar_path;
156 | }
157 |
158 | private void DecompileJar(JavaConfig config, string jar_path, string folder)
159 | {
160 | Profiler.Start($"Decompiling");
161 | if (config.Decompiler == DecompilerType.Cfr)
162 | {
163 | var result = CommandRunner.RunJavaCommand(folder, config.JavaInstallationPaths,
164 | $"{config.DecompilerArgs} -jar \"{config.CfrPath}\" \"{jar_path}\" " +
165 | $"--outputdir \"{folder}\" {config.CfrArgs}");
166 | if (result.ExitCode != 0)
167 | throw new ApplicationException($"Failed to decompile: {result.Error}");
168 | string summary_file = Path.Combine(folder, "summary.txt");
169 | if (File.Exists(summary_file))
170 | {
171 | Console.WriteLine("Summary:");
172 | Console.WriteLine(File.ReadAllText(summary_file));
173 | File.Delete(summary_file);
174 | }
175 | }
176 | else if (config.Decompiler == DecompilerType.Fernflower)
177 | {
178 | string output_dir = Path.Combine(folder, "decompiled");
179 | Directory.CreateDirectory(output_dir);
180 | var result = CommandRunner.RunJavaCommand(folder, config.JavaInstallationPaths,
181 | $"{config.DecompilerArgs} -jar \"{config.FernflowerPath}\" " +
182 | $"{config.FernflowerArgs} \"{jar_path}\" \"{output_dir}\"");
183 | ;
184 | if (result.ExitCode != 0)
185 | throw new ApplicationException($"Failed to decompile: {result.Error}");
186 | using (ZipArchive zip = ZipFile.OpenRead(Path.Combine(output_dir, Path.GetFileName(jar_path))))
187 | {
188 | foreach (var entry in zip.Entries)
189 | {
190 | Directory.CreateDirectory(Path.Combine(folder, Path.GetDirectoryName(entry.FullName)));
191 | entry.ExtractToFile(Path.Combine(folder, entry.FullName));
192 | }
193 | }
194 |
195 | Directory.Delete(output_dir, true);
196 | }
197 | else if (config.Decompiler == DecompilerType.FernflowerUnzipped)
198 | {
199 | var result = CommandRunner.RunJavaCommand(folder, config.JavaInstallationPaths,
200 | $"{config.DecompilerArgs} -jar \"{config.FernflowerPath}\" " +
201 | $"{config.FernflowerArgs} \"{jar_path}\" \"{folder}\"");
202 | ;
203 | if (result.ExitCode != 0)
204 | throw new ApplicationException($"Failed to decompile: {result.Error}");
205 | Directory.Delete(Path.Combine(folder, "assets"), true);
206 | Directory.Delete(Path.Combine(folder, "data"), true);
207 | foreach (var file in Directory.GetFiles(folder, "*", SearchOption.TopDirectoryOnly))
208 | {
209 | File.Delete(file);
210 | }
211 | }
212 | else
213 | throw new ArgumentException(nameof(config.Decompiler));
214 |
215 | Profiler.Stop();
216 | }
217 |
218 | private string[] ReadClassPath(ZipArchive archive)
219 | {
220 | var entry = archive.GetEntry("META-INF/classpath-joined");
221 | if (entry != null)
222 | {
223 | using var stream = entry.Open();
224 | using var reader = new StreamReader(stream);
225 | var files = reader.ReadToEnd().Split(";");
226 | return files;
227 | }
228 |
229 | return null;
230 | }
231 |
232 | private const string LAUNCHER_PATCH_NOTES = "https://launchercontent.mojang.com/v2/javaPatchNotes.json";
233 | private static Dictionary CachedPatchNotes;
234 |
235 | private void DownloadPatchNotes(string folder)
236 | {
237 | if (CachedPatchNotes == null)
238 | {
239 | CachedPatchNotes = new();
240 | var note_list = (JsonArray)JsonNode.Parse(Util.DownloadString(LAUNCHER_PATCH_NOTES))["entries"];
241 | foreach (var entry in note_list)
242 | {
243 | CachedPatchNotes[entry["version"].GetValue()] = (JsonObject)entry;
244 | }
245 | }
246 |
247 | if (CachedPatchNotes.TryGetValue(this.Name, out var data))
248 | {
249 | Profiler.Start("Downloading patch notes");
250 | Directory.CreateDirectory(folder);
251 | var notes = (JsonObject)JsonNode.Parse(Util.DownloadString("https://launchercontent.mojang.com/v2/" + data["contentPath"].GetValue()));
252 | string image_url = "https://launchercontent.mojang.com" + notes["image"]["url"].GetValue();
253 | string image_name = "image" + Path.GetExtension(image_url);
254 | Util.DownloadFile(image_url, Path.Combine(folder, image_name));
255 | string title = notes["title"].GetValue();
256 | string html = $"{title}
\n
\n" + notes["body"].GetValue();
257 | File.WriteAllText(Path.Combine(folder, "blog.html"), html);
258 | Profiler.Stop();
259 | }
260 | else
261 | Console.WriteLine("No patch notes found!");
262 | }
263 |
264 | private void DecompileMinecraft(JavaConfig config, string destination)
265 | {
266 | Directory.CreateDirectory(destination);
267 | string final_jar = Path.Combine(destination, $"{Path.GetFileNameWithoutExtension(Client.JarPath)}_final.jar");
268 | string mapped_client = MapJar(config, Client, destination);
269 | string used_client = mapped_client ?? Client.JarPath;
270 | string mapped_server = null;
271 | string unbundled_server_path = null;
272 | DownloadServerJar(config);
273 | string final_server_jar = Server.JarPath;
274 | if (Server.JarPath is not null)
275 | {
276 | using (ZipArchive archive = ZipFile.Open(Server.JarPath, ZipArchiveMode.Read))
277 | {
278 | var libraries = ReadClassPath(archive);
279 | if (libraries != null)
280 | {
281 | Profiler.Start("Unbundling server");
282 |
283 | unbundled_server_path = Path.Combine(destination,
284 | $"{Path.GetFileNameWithoutExtension(Server.JarPath)}_unbundled.jar");
285 | final_server_jar = unbundled_server_path;
286 | using ZipArchive unbundled = ZipFile.Open(unbundled_server_path, ZipArchiveMode.Create);
287 |
288 | foreach (var library in libraries)
289 | {
290 | var bundled_jar = archive.GetEntry("META-INF/" + library);
291 | using var jar_stream = bundled_jar.Open();
292 | using var jar_archive = new ZipArchive(jar_stream);
293 | CombineArchives(jar_archive, unbundled);
294 | }
295 |
296 | foreach (var entry in archive.Entries)
297 | {
298 | if (!entry.FullName.StartsWith("META-INF/"))
299 | CopyEntry(entry, unbundled);
300 | }
301 |
302 | Profiler.Stop();
303 | }
304 | }
305 |
306 | mapped_server = MapJar(config, Server with { JarPath = final_server_jar }, destination);
307 | string used_server = mapped_server ?? final_server_jar;
308 | CombineJars(final_jar, used_client, used_server);
309 | }
310 | else
311 | File.Copy(used_client, final_jar);
312 |
313 | Profiler.Start("Processing final jar files");
314 | // use the old-style using since the zip needs to dispose before decompiling starts
315 | using (ZipArchive archive = ZipFile.Open(final_jar, ZipArchiveMode.Update))
316 | {
317 | foreach (var entry in archive.Entries.ToList())
318 | {
319 | if (entry.FullName.EndsWith("/"))
320 | continue;
321 | if (config.ExcludeDecompiledEntry(entry.FullName))
322 | entry.Delete();
323 | }
324 | }
325 |
326 | Profiler.Stop();
327 |
328 | DecompileJar(config, final_jar, destination);
329 |
330 | if (mapped_client is not null)
331 | File.Delete(mapped_client);
332 | if (mapped_server is not null)
333 | File.Delete(mapped_server);
334 | if (unbundled_server_path is not null)
335 | File.Delete(unbundled_server_path);
336 | File.Delete(final_jar);
337 | }
338 |
339 | private void CombineArchives(ZipArchive source_zip, ZipArchive destination)
340 | {
341 | foreach (var item in source_zip.Entries)
342 | {
343 | CopyEntry(item, destination);
344 | }
345 | }
346 |
347 | private void CopyEntry(ZipArchiveEntry entry, ZipArchive destination)
348 | {
349 | var file = destination.CreateEntry(entry.FullName);
350 | using var source = entry.Open();
351 | using var dest = file.Open();
352 | source.CopyTo(dest);
353 | }
354 |
355 | private void CombineJars(string destination, params string[] paths)
356 | {
357 | Profiler.Start($"Combining {paths.Length} jar files");
358 | using var archive = ZipFile.Open(destination, ZipArchiveMode.Create);
359 | foreach (var path in paths)
360 | {
361 | using ZipArchive zip = ZipFile.OpenRead(path);
362 | CombineArchives(zip, archive);
363 | }
364 |
365 | Profiler.Stop();
366 | }
367 |
368 | public void DownloadServerJar(JavaConfig config)
369 | {
370 | if (ServerJarURL is null)
371 | return;
372 | string path = Path.Combine(config.ServerJarFolder, Name + ".jar");
373 | if (Server.JarPath is null)
374 | Server = Server with { JarPath = path };
375 | if (!File.Exists(path))
376 | {
377 | Util.DownloadFile(ServerJarURL, path);
378 | Server = Server with { JarPath = path };
379 | }
380 | }
381 | }
--------------------------------------------------------------------------------
/MCPModernizer/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Nodes;
3 | using MinecraftVersionHistory;
4 | using TryashtarUtils.Utility;
5 | using YamlDotNet.RepresentationModel;
6 |
7 | namespace MCPModernizer;
8 |
9 | enum ArgType
10 | {
11 | Classic,
12 | ModernSRG,
13 | ModernCSV,
14 | Output
15 | }
16 | // the most schizophrenic code you've ever seen in your life
17 | public static class Program
18 | {
19 | public static void Main(string[] args)
20 | {
21 | ArgType? parsing = null;
22 | var sorted = new Dictionary>
23 | {
24 | [ArgType.Classic] = new(),
25 | [ArgType.ModernSRG] = new(),
26 | [ArgType.ModernCSV] = new(),
27 | [ArgType.Output] = new()
28 | };
29 | foreach (var arg in args)
30 | {
31 | if (arg.StartsWith("--"))
32 | {
33 | parsing = arg switch
34 | {
35 | "--classic" => ArgType.Classic,
36 | "--modern-srg" => ArgType.ModernSRG,
37 | "--modern-csv" => ArgType.ModernCSV,
38 | "--output" => ArgType.Output,
39 | _ => throw new ArgumentException()
40 | };
41 | }
42 | else if (parsing != null)
43 | sorted[parsing.Value].Add(arg);
44 | }
45 | var versioned_map = new VersionedRenames();
46 | var mcps = new Dictionary();
47 | void add_mcp(MCP mcp)
48 | {
49 | if (mcps.TryGetValue(mcp.ClientVersion, out var existing))
50 | {
51 | if (MCP.Sorter.Compare(mcp, existing) > 0)
52 | Merge2MCPs(mcps[mcp.ClientVersion], mcp);
53 | else
54 | MergeMCPs(mcps[mcp.ClientVersion], mcp, mcps[mcp.ClientVersion]);
55 | }
56 | else
57 | mcps[mcp.ClientVersion] = mcp;
58 | }
59 | static void MergeMCPs(MCP destination, MCP mcp1, MCP mcp2)
60 | {
61 | Merge2MCPs(destination, mcp1);
62 | Merge2MCPs(destination, mcp2);
63 | }
64 | static void Merge2MCPs(MCP destination, MCP mcp1)
65 | {
66 | Console.WriteLine($"Merging {destination.ClientVersion} and {mcp1.ClientVersion}");
67 | foreach (var item in mcp1.LocalMappings.Client.ClassList.ToList())
68 | {
69 | var cl = destination.LocalMappings.Client.AddClass(item.OldName, item.NewName);
70 | foreach (var i in item.FieldList.ToList())
71 | {
72 | cl.AddField(i.OldName, i.NewName);
73 | }
74 | foreach (var i in item.MethodList.ToList())
75 | {
76 | cl.AddMethod(i.OldName, i.NewName, i.Signature);
77 | }
78 | }
79 | foreach (var item in mcp1.LocalMappings.Server.ClassList.ToList())
80 | {
81 | var cl = destination.LocalMappings.Server.AddClass(item.OldName, item.NewName);
82 | foreach (var i in item.FieldList.ToList())
83 | {
84 | cl.AddField(i.OldName, i.NewName);
85 | }
86 | foreach (var i in item.MethodList.ToList())
87 | {
88 | cl.AddMethod(i.OldName, i.NewName, i.Signature);
89 | }
90 | }
91 | foreach (var item in mcp1.FriendlyNames.Client.ClassMap.ToList())
92 | {
93 | destination.FriendlyNames.Client.AddClass(item.OldName, item.NewName);
94 | }
95 | foreach (var item in mcp1.FriendlyNames.Client.MethodMap.ToList())
96 | {
97 | destination.FriendlyNames.Client.AddMethod(item.OldName, item.NewName);
98 | }
99 | foreach (var item in mcp1.FriendlyNames.Client.FieldMap.ToList())
100 | {
101 | destination.FriendlyNames.Client.AddField(item.OldName, item.NewName);
102 | }
103 | foreach (var item in mcp1.FriendlyNames.Server.ClassMap.ToList())
104 | {
105 | destination.FriendlyNames.Server.AddClass(item.OldName, item.NewName);
106 | }
107 | foreach (var item in mcp1.FriendlyNames.Server.MethodMap.ToList())
108 | {
109 | destination.FriendlyNames.Server.AddMethod(item.OldName, item.NewName);
110 | }
111 | foreach (var item in mcp1.FriendlyNames.Server.FieldMap.ToList())
112 | {
113 | destination.FriendlyNames.Server.AddField(item.OldName, item.NewName);
114 | }
115 | Console.WriteLine("Merge done");
116 | }
117 | foreach (var folder in sorted[ArgType.Classic])
118 | {
119 | var fallback = new Dictionary();
120 | var version_map_file = Path.Combine(folder, "versions.yaml");
121 | if (File.Exists(version_map_file))
122 | fallback = YamlHelper.ParseFile(version_map_file).ToDictionary();
123 | foreach (var file in Directory.GetFiles(folder, "*.zip"))
124 | {
125 | var mcp = new ClassicMCP(file, fallback);
126 | Console.WriteLine($"Classic MCP for {mcp.ClientVersion}");
127 | add_mcp(mcp);
128 | }
129 | }
130 |
131 | var jsons = new Dictionary();
132 | foreach (var folder in sorted[ArgType.ModernCSV])
133 | {
134 | var file = Path.Combine(folder, "versions.json");
135 | if (File.Exists(file))
136 | {
137 | using var reader = File.OpenRead(file);
138 | jsons.Add(JsonSerializer.Deserialize(reader)!, folder);
139 | }
140 | }
141 | void find_version(string parent, string version, string series)
142 | {
143 | string tsrg = Path.Combine(parent, "joined.tsrg");
144 | if (File.Exists(tsrg))
145 | {
146 | foreach (var (json, folder) in jsons)
147 | {
148 | IEnumerable<(string type, string number)> choose()
149 | {
150 | if (!(json.TryGetPropertyValue(series, out var r) && r is JsonObject results))
151 | yield break;
152 | if (results.TryGetPropertyValue("snapshot", out var n) && n is JsonArray snapshot)
153 | {
154 | foreach (var item in snapshot.Select(x => ("snapshot", x!.ToString())).Reverse())
155 | {
156 | yield return item;
157 | }
158 | }
159 | if (results.TryGetPropertyValue("stable", out var t) && t is JsonArray stable)
160 | {
161 | foreach (var item in stable.Select(x => ("stable", x!.ToString())).Reverse())
162 | {
163 | yield return item;
164 | }
165 | }
166 | }
167 |
168 | var choices = choose().ToList();
169 | if (choices.Any())
170 | {
171 | var csvs = choices.Select(x => Path.Combine(folder, $"mcp_{x.type}", $"{x.number}-{series}", $"mcp_{x.type}-{x.number}-{series}.zip"));
172 | var mcp = new ModernMCP(version, series, tsrg, csvs.ToArray());
173 | Console.WriteLine($"Modern MCP for {mcp.ClientVersion}");
174 | add_mcp(mcp);
175 | }
176 | else
177 | Console.WriteLine($"Couldn't find CSVs for {version} in {series}");
178 | }
179 | }
180 | }
181 | foreach (var folder in sorted[ArgType.ModernSRG])
182 | {
183 | string[] types = { "pre", "release", "snapshot" };
184 | foreach (var type in types)
185 | {
186 | foreach (var sub in Directory.GetDirectories(Path.Combine(folder, type)))
187 | {
188 | if (type == "release")
189 | find_version(sub, Path.GetFileName(sub), Path.GetFileName(sub));
190 | else
191 | {
192 | foreach (var deeper in Directory.GetDirectories(sub))
193 | {
194 | if (type == "snapshot")
195 | find_version(deeper, Path.GetFileName(deeper), Path.GetFileName(sub));
196 | else if (type == "pre")
197 | {
198 | string version = Path.GetFileName(deeper);
199 | int dash = version.IndexOf('-');
200 | string series = version[..dash];
201 | find_version(deeper, version, series);
202 | }
203 | }
204 | }
205 | }
206 | }
207 | }
208 |
209 | var class_renames = new Dictionary<(Rename rename, string side), HashSet>();
210 | var field_renames = new Dictionary<(Rename rename, string side), HashSet>();
211 | var method_renames = new Dictionary<(Rename rename, string side), HashSet>();
212 | var latest = new Sided();
213 | var equivs = new Sided();
214 | var not_found_report = new Dictionary>();
215 | static void add_to(Dictionary<(Rename, string), HashSet> dict, IEnumerable stuff, string side, string version)
216 | {
217 | foreach (var item in stuff)
218 | {
219 | if (!dict.ContainsKey((item, side)))
220 | dict[(item, side)] = new();
221 | dict[(item, side)].Add(version);
222 | }
223 | }
224 |
225 | foreach (var mcp in mcps.Values.OrderBy(x => x, new HistoryComparer()))
226 | {
227 | Console.WriteLine(mcp.ClientVersion);
228 | add_to(class_renames, mcp.FriendlyNames.Client.ClassMap, "client", mcp.ClientVersion);
229 | add_to(class_renames, mcp.FriendlyNames.Server.ClassMap, "server", mcp.ClientVersion);
230 | add_to(field_renames, mcp.FriendlyNames.Client.FieldMap, "client", mcp.ClientVersion);
231 | add_to(field_renames, mcp.FriendlyNames.Server.FieldMap, "server", mcp.ClientVersion);
232 | add_to(method_renames, mcp.FriendlyNames.Client.MethodMap, "client", mcp.ClientVersion);
233 | add_to(method_renames, mcp.FriendlyNames.Server.MethodMap, "server", mcp.ClientVersion);
234 | foreach (var item in mcp.FriendlyNames.Client.ClassMap)
235 | {
236 | latest.Client.AddClass(item.OldName, item.NewName);
237 | }
238 | foreach (var item in mcp.FriendlyNames.Client.FieldMap)
239 | {
240 | latest.Client.AddField(item.OldName, item.NewName);
241 | }
242 | foreach (var item in mcp.FriendlyNames.Client.MethodMap)
243 | {
244 | latest.Client.AddMethod(item.OldName, item.NewName);
245 | }
246 | foreach (var item in mcp.FriendlyNames.Server.ClassMap)
247 | {
248 | latest.Server.AddClass(item.OldName, item.NewName);
249 | }
250 | foreach (var item in mcp.FriendlyNames.Server.FieldMap)
251 | {
252 | latest.Server.AddField(item.OldName, item.NewName);
253 | }
254 | foreach (var item in mcp.FriendlyNames.Server.MethodMap)
255 | {
256 | latest.Server.AddMethod(item.OldName, item.NewName);
257 | }
258 | if (mcp is ClassicMCP classic)
259 | {
260 | foreach (var (from, to) in classic.NewIDs.Client)
261 | {
262 | if (from.StartsWith("field_"))
263 | equivs.Client.AddFields(new[] { from, to });
264 | else if (from.StartsWith("func_"))
265 | equivs.Client.AddMethods(new[] { from, to });
266 | }
267 | foreach (var (from, to) in classic.NewIDs.Server)
268 | {
269 | if (from.StartsWith("field_"))
270 | equivs.Server.AddFields(new[] { from, to });
271 | else if (from.StartsWith("func_"))
272 | equivs.Server.AddMethods(new[] { from, to });
273 | }
274 | }
275 | foreach (var output in sorted[ArgType.Output])
276 | {
277 | string dir = Path.Combine(output, mcp.ClientVersion);
278 | Directory.CreateDirectory(dir);
279 | mcp.WriteClientMappings(Path.Combine(dir, "client.tsrg"));
280 | mcp.WriteServerMappings(Path.Combine(dir, "server.tsrg"));
281 | }
282 | }
283 | var reversed_dict = new Dictionary, Sided>(HashSet.CreateSetComparer());
284 | void apply(Dictionary<(Rename rename, string side), HashSet> dict, Action adder)
285 | {
286 | foreach (var pair in dict)
287 | {
288 | if (!reversed_dict.ContainsKey(pair.Value))
289 | reversed_dict[pair.Value] = new();
290 | if (pair.Key.side == "client")
291 | adder(reversed_dict[pair.Value].Client, pair.Key.rename.OldName, pair.Key.rename.NewName);
292 | else if (pair.Key.side == "server")
293 | adder(reversed_dict[pair.Value].Server, pair.Key.rename.OldName, pair.Key.rename.NewName);
294 | }
295 | }
296 | apply(class_renames, (x, y, z) => x.AddClass(y, z));
297 | apply(field_renames, (x, y, z) => x.AddField(y, z));
298 | apply(method_renames, (x, y, z) => x.AddMethod(y, z));
299 | foreach (var (versions, map) in reversed_dict)
300 | {
301 | versioned_map.Add(new(versions), map);
302 | }
303 |
304 | versioned_map.Add(VersionSpec.All, latest);
305 | foreach (var output in sorted[ArgType.Output])
306 | {
307 | versioned_map.WriteTo(Path.Combine(output, "mappings_found.yaml"));
308 | Equivalencies.WriteTo(equivs, Path.Combine(output, "equivalencies_found.yaml"));
309 | }
310 |
311 | foreach (var mcp in mcps.Values)
312 | {
313 | Console.WriteLine(mcp.ClientVersion);
314 | foreach (var c in mcp.LocalMappings.Client.ClassList)
315 | {
316 | var end_name = c.NewName.Split('.')[^1];
317 | if (end_name.StartsWith("C_"))
318 | {
319 | if (!not_found_report.ContainsKey(c.NewName))
320 | not_found_report[c.NewName] = new();
321 | not_found_report[c.NewName][mcp.ClientVersion] = c.NewName;
322 | }
323 | foreach (var m in c.MethodList)
324 | {
325 | if (!m.NewName.StartsWith("func_"))
326 | continue;
327 | var match = versioned_map.GetClientMethod(mcp.ClientVersion, m.NewName, equivs.Client);
328 | if (match == null)
329 | {
330 | if (!not_found_report.ContainsKey(m.NewName))
331 | not_found_report[m.NewName] = new();
332 | not_found_report[m.NewName][mcp.ClientVersion] = c.NewName;
333 | }
334 | }
335 | }
336 | foreach (var c in mcp.LocalMappings.Server.ClassList)
337 | {
338 | var end_name = c.NewName.Split('.')[^1];
339 | if (end_name.StartsWith("C_"))
340 | {
341 | if (!not_found_report.ContainsKey(c.NewName))
342 | not_found_report[c.NewName] = new();
343 | not_found_report[c.NewName][mcp.ClientVersion] = c.NewName;
344 | }
345 | foreach (var m in c.MethodList)
346 | {
347 | if (!m.NewName.StartsWith("func_"))
348 | continue;
349 | var match = versioned_map.GetServerMethod(mcp.ClientVersion, m.NewName, equivs.Server);
350 | if (match == null)
351 | {
352 | if (!not_found_report.ContainsKey(m.NewName))
353 | not_found_report[m.NewName] = new();
354 | not_found_report[m.NewName][mcp.ClientVersion] = c.NewName;
355 | }
356 | }
357 | }
358 | foreach (var c in mcp.LocalMappings.Client.ClassList)
359 | {
360 | foreach (var m in c.FieldList)
361 | {
362 | if (!m.NewName.StartsWith("field_"))
363 | continue;
364 | var match = versioned_map.GetClientField(mcp.ClientVersion, m.NewName, equivs.Client);
365 | if (match == null)
366 | {
367 | if (!not_found_report.ContainsKey(m.NewName))
368 | not_found_report[m.NewName] = new();
369 | not_found_report[m.NewName][mcp.ClientVersion] = c.NewName;
370 | }
371 | }
372 | }
373 | foreach (var c in mcp.LocalMappings.Server.ClassList)
374 | {
375 | foreach (var m in c.FieldList)
376 | {
377 | if (!m.NewName.StartsWith("field_"))
378 | continue;
379 | var match = versioned_map.GetServerField(mcp.ClientVersion, m.NewName, equivs.Server);
380 | if (match == null)
381 | {
382 | if (!not_found_report.ContainsKey(m.NewName))
383 | not_found_report[m.NewName] = new();
384 | not_found_report[m.NewName][mcp.ClientVersion] = c.NewName;
385 | }
386 | }
387 | }
388 | }
389 | var nfp = new YamlMappingNode();
390 | foreach (var pair in not_found_report.OrderBy(x => x.Value.Count).ThenBy(x => x.Key))
391 | {
392 | var v = new YamlMappingNode();
393 | foreach (var pair2 in pair.Value)
394 | {
395 | v.Add(pair2.Key, pair2.Value);
396 | }
397 | if (!pair.Value.ContainsKey("1.14.4"))
398 | nfp.Add(pair.Key, v);
399 | }
400 | foreach (var output in sorted[ArgType.Output])
401 | {
402 | YamlHelper.SaveToFile(nfp, Path.Combine(output, "missing.yaml"));
403 | }
404 | }
405 | class HistoryComparer : IComparer
406 | {
407 | public int Compare(MCP? x, MCP? y)
408 | {
409 | int c = CompareSeries(GetSeries(x!), GetSeries(y!));
410 | if (c != 0)
411 | return c;
412 | return CompareVersions(x!, y!);
413 | }
414 |
415 | private int CompareVersions(MCP x, MCP y)
416 | {
417 | if (x.ClientVersion.Contains("w") && y.ClientVersion.Contains("w"))
418 | return CompareSnapshots(x.ClientVersion, y.ClientVersion);
419 | if (x.ClientVersion.Contains("w"))
420 | return -1;
421 | if (y.ClientVersion.Contains("w"))
422 | return 1;
423 | return CompareSeries(x.ClientVersion, y.ClientVersion);
424 | }
425 |
426 | private int CompareSnapshots(string x, string y)
427 | {
428 | return String.Compare(x, y, StringComparison.Ordinal);
429 | }
430 |
431 | private int CompareSeries(string x, string y)
432 | {
433 | string[] xs = x.Split('.');
434 | string[] ys = y.Split('.');
435 | if (x.StartsWith("a"))
436 | {
437 | if (y.StartsWith("b") || !y.StartsWith("a"))
438 | return -1;
439 | }
440 | if (y.StartsWith("a"))
441 | {
442 | if (x.StartsWith("b") || !x.StartsWith("a"))
443 | return 1;
444 | }
445 | if (x.StartsWith("b"))
446 | {
447 | if (y.StartsWith("a"))
448 | return 1;
449 | if (!y.StartsWith("b"))
450 | return -1;
451 | }
452 | if (y.StartsWith("b"))
453 | {
454 | if (x.StartsWith("a"))
455 | return -1;
456 | if (!x.StartsWith("b"))
457 | return 1;
458 | }
459 | for (int i = 0; i < Math.Min(xs.Length, ys.Length); i++)
460 | {
461 |
462 | if (xs[i].Contains("-pre") && ys[i].Contains("-pre"))
463 | {
464 | xs[i] = xs[i].Replace("-pre", "");
465 | ys[i] = ys[i].Replace("-pre", "");
466 | }
467 | if (int.TryParse(xs[i], out int xsi) && int.TryParse(ys[i], out int ysi))
468 | {
469 | int ic = xsi.CompareTo(ysi);
470 | if (ic != 0)
471 | return ic;
472 | }
473 | int c = String.Compare(xs[i], ys[i], StringComparison.Ordinal);
474 | if (c != 0)
475 | return c;
476 | }
477 | int c2 = xs.Length.CompareTo(ys.Length);
478 | return c2;
479 | }
480 |
481 | private string GetSeries(MCP m)
482 | {
483 | if (m.ClientVersion.StartsWith("12w"))
484 | return "1.3";
485 | if (m.ClientVersion.StartsWith("13w"))
486 | return "1.5";
487 | if (m is ClassicMCP c)
488 | return c.ClientVersion;
489 | else if (m is ModernMCP a)
490 | return a.Series;
491 | else throw new Exception();
492 | }
493 | }
494 | }
--------------------------------------------------------------------------------