const std = @import("std"); /// Minecraft launcher! pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = false }){}; // TODO turn safety back on defer _ = gpa.deinit(); const allocator = gpa.allocator(); var client = std.http.Client{ .allocator = allocator }; defer client.deinit(); const manifest_wrap = try download_json(&client, Manifest, Manifest.uri); defer manifest_wrap.deinit(); const manifest = manifest_wrap.value; std.log.info("release: {s} snapshot: {s}", .{ manifest.latest.release, manifest.latest.snapshot }); // Find the release version const release_version_url = for (manifest.versions) |v| { if (std.mem.eql(u8, v.id, manifest.latest.release)) { break v.url; } } else { return error.NoReleaseVersionFound; }; var release_version_wrap = try download_json(&client, Version, release_version_url); defer release_version_wrap.deinit(); const release_version = release_version_wrap.value; _ = release_version; // Download all the bits // std.mem.replace(comptime T: type, input: []const T, needle: []const T, replacement: []const T, output: []T) // check the latest.release // find that in the versions[] by key id // get the download URL for that https://piston-meta.mojang.com/v1/packages/1f376e6cb981265af345076889f9ef325bb6b9e8/1.20.1.json // get arguments.game and arguments.jvm ready to supply ... // probably just use the system java version for now // } /// https://minecraft.fandom.com/wiki/Version_manifest.json const Manifest = struct { const uri = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; latest: struct { release: []const u8, snapshot: []const u8, }, versions: []struct { id: []const u8, type: []const u8, url: []const u8, releaseTime: []const u8, sha1: []const u8, complianceLevel: i64, }, }; /// https://minecraft.fandom.com/wiki/Client.json const Version = struct { const Rule = struct { const Feature = struct { is_demo_user: bool = false, has_custom_resolution: bool = false, is_quick_play_multiplayer: bool = false, is_quick_play_realms: bool = false, }; const Os = struct { name: ?[]const u8 = null, arch: ?[]const u8 = null, }; action: enum{allow}, features: ?Feature = null, os: ?Os = null, }; const Arg = union(enum) { const Extended = struct { const Value = union(enum) { single: []const u8, many: []const []const u8, pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Value { switch (try source.peekNextTokenType()) { .array_begin => { return .{ .many = try std.json.innerParse([]const []const u8, allocator, source, options) }; }, .string => { return .{ .single = try std.json.innerParse([]const u8, allocator, source, options) }; }, else => return error.UnexpectedToken, } } }; rules: []Rule, value: Value, }; plain: []const u8, extended: Extended, pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Arg { switch (try source.peekNextTokenType()) { .object_begin => { return .{ .extended = try std.json.innerParse(Extended, allocator, source, options), }; }, .string => { return .{ .plain = try std.json.innerParse([]const u8, allocator, source, options), }; }, else => return error.UnexpectedToken, } } }; const Download = struct { sha1: []const u8, size: u64, url: []const u8, }; const Library = struct { downloads: struct { artifact: struct { path: []const u8, sha1: []const u8, size: u64, url: []const u8, }, }, name: []const u8, rules: ?[]Rule = null, }; arguments: struct { game: []Arg, jvm: []Arg, }, assetIndex: struct { id:[]const u8, sha1: []const u8, size: u64, totalSize: u64, url: []const u8, }, assets: []const u8, complianceLevel: u8, downloads: struct { client: Download, client_mappings: Download, server: Download, server_mappings: Download, }, id: []const u8, javaVersion: struct { component: []const u8, majorVersion: u8, }, libraries: []Library, logging: struct { client: struct { argument: []const u8, file: struct { id: []const u8, sha1: []const u8, size: u64, url: []const u8, }, type: []const u8, }, }, mainClass: []const u8, minimumLauncherVersion: u8, releaseTime: []const u8, time: []const u8, type: []const u8, }; /// Caller is responsible for calling deinit on the resulting Parsed struct. fn download_json(client: *std.http.Client, comptime T: type, url_maybe_https: []const u8) !std.json.Parsed(T) { // TODO use https once zig supports TLSv1.2 or mojang support TLSv1.3 const a = client.allocator; const sz = std.mem.replacementSize(u8, url_maybe_https, "https://", "http://"); const url = try a.alloc(u8, sz); defer a.free(url); _ = std.mem.replace(u8, url_maybe_https, "https://", "http://", url); var h = std.http.Headers.init(a); defer h.deinit(); try h.append("Accept", "application/json"); const uri = try std.Uri.parse(url); var r = try client.request(.GET, uri, h, .{}); try r.start(); try r.wait(); if (r.response.status != .ok) { std.log.err("request failed {}:{s}", .{ r.response.status, r.response.reason }); return error.HttpStatusError; } var r2 = std.json.reader(a, r.reader()); var diags = std.json.Diagnostics{}; r2.enableDiagnostics(&diags); defer r2.deinit(); var res = std.json.parseFromTokenSource(T, a, &r2, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }) catch |e| { std.log.err("parse failed {} line {} col {}", .{ e, diags.getLine(), diags.getColumn() }); return e; }; return res; } test "heterogenous array" { const x = \\ { \\ "y": [ \\ { "z": "yay" }, \\ "nay" \\ ] \\ } ; const IX = union(enum) { // What to name union tags? const P = struct { z: []const u8 }; p: P, q: []const u8, pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { switch (try source.peekNextTokenType()) { .object_begin => { return .{ .p = try std.json.innerParse(P, allocator, source, options), }; }, .string => { return .{ .q = try std.json.innerParse([]const u8, allocator, source, options), }; }, else => return error.UnexpectedToken, } } }; const X = struct { y: []const IX }; const xx = try std.json.parseFromSlice(X, std.testing.allocator, x, .{}); defer xx.deinit(); try std.testing.expectEqualDeep(X{ .y = &[_]IX{ .{ .p = .{ .z = "yay" } }, .{ .q = "nay" }, }, }, xx.value); } test "string list" { const x = \\["ond", "two", "three", "four"] ; const v_w = try std.json.parseFromSlice([]const []const u8, std.testing.allocator, x, .{}); defer v_w.deinit(); const expect: []const []const u8 = &[_][]const u8{ "ond", "two", "three", "four" }; try std.testing.expectEqualDeep(expect, v_w.value); } test "string or string list" { const x1 = \\ "testing" ; const x2 = \\ ["one", "two"] ; const X = union(enum) { one: []const u8, many: []const []const u8, pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { switch (try source.peekNextTokenType()) { .array_begin => { return .{ .many = try std.json.innerParse([]const []const u8, allocator, source, options) }; }, .string => { return .{ .one = try std.json.innerParse([]const u8, allocator, source, options) }; }, else => return error.UnexpectedToken, } } }; var ax1_w = try std.json.parseFromSlice(X, std.testing.allocator, x1, .{}); defer ax1_w.deinit(); var ax2_w = try std.json.parseFromSlice(X, std.testing.allocator, x2, .{}); defer ax2_w.deinit(); } test "Version" { const x = @embedFile("version_test.json"); var f = std.io.fixedBufferStream(x); var jr = std.json.reader(std.testing.allocator, f.reader()); defer jr.deinit(); var d = std.json.Diagnostics{}; jr.enableDiagnostics(&d); var v_w = std.json.parseFromTokenSource(Version, std.testing.allocator, &jr, .{.ignore_unknown_fields = true}) catch |e| { std.log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() }); return e; }; defer v_w.deinit(); const v = v_w.value; _ = v; // v.arguments.game }