Skip to content

Commit f4a2f66

Browse files
committed
feat: Ability to have a specially named executable to install a certain modpack and version (or latest)
fixes #18, fixes #19
1 parent 296e3cf commit f4a2f66

File tree

5 files changed

+166
-57
lines changed

5 files changed

+166
-57
lines changed

CurseForge.Minecraft.Serverpack.Launcher/Classes/CurseForgeFile.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace CurseForge.Minecraft.Serverpack.Launcher
55
public class CurseForgeFile
66
{
77
[JsonPropertyName("projectID")]
8-
public long ProjectId { get; set; }
8+
public uint ProjectId { get; set; }
99
[JsonPropertyName("fileID")]
10-
public long FileId { get; set; }
10+
public uint FileId { get; set; }
1111
[JsonPropertyName("required")]
1212
public bool Required { get; set; }
1313
}

CurseForge.Minecraft.Serverpack.Launcher/CommandArguments.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ private static RootCommand SetupCommand()
1919
SetupArguments(command);
2020
SetupOptions(command);
2121

22-
command.Handler = CommandHandler.Create<int, int, string, string, bool>(async (projectid, fileid, serverPath, javaArgs, startServer) => {
22+
command.Handler = CommandHandler.Create<uint, uint, string, string, bool>(async (projectid, fileid, serverPath, javaArgs, startServer) => {
2323
return await InstallServer(projectid, fileid, serverPath, javaArgs, startServer);
2424
});
2525

@@ -32,8 +32,29 @@ private static void SetupSubcommand(RootCommand command)
3232
description: @"The interactive mode lets you search and select what modpack you want to use.
3333
This will search for modpacks from CurseForge.");
3434

35-
interactive.Handler = CommandHandler.Create(async () => {
36-
return await InteractiveInstallation();
35+
interactive.AddArgument(new("automaticInstaller")
36+
{
37+
ArgumentType = typeof(bool),
38+
Arity = ArgumentArity.ZeroOrOne,
39+
Description = "Runs the installer even more automatic"
40+
});
41+
42+
interactive.AddArgument(new("projectId")
43+
{
44+
ArgumentType = typeof(uint),
45+
Arity = ArgumentArity.ZeroOrOne,
46+
Description = "ProjectId for the modpack"
47+
});
48+
49+
interactive.AddArgument(new("fileId")
50+
{
51+
ArgumentType = typeof(string),
52+
Arity = ArgumentArity.ZeroOrOne,
53+
Description = "FileId (or \"latest\") for the modpack"
54+
});
55+
56+
interactive.Handler = CommandHandler.Create<bool?, uint?, string>(async (automaticInstaller, projectId, fileId) => {
57+
return await InteractiveInstallation(automaticInstaller, projectId, fileId);
3758
});
3859

3960
command.Add(interactive);
@@ -102,14 +123,14 @@ private static void SetupArguments(RootCommand command)
102123
{
103124
command.AddArgument(new("projectid")
104125
{
105-
ArgumentType = typeof(int),
126+
ArgumentType = typeof(uint),
106127
Arity = ArgumentArity.ZeroOrOne,
107128
Description = "Sets the project id / modpack id to use",
108129
});
109130

110131
command.AddArgument(new("fileid")
111132
{
112-
ArgumentType = typeof(int),
133+
ArgumentType = typeof(uint),
113134
Description = "Sets the file id to use"
114135
});
115136

CurseForge.Minecraft.Serverpack.Launcher/InteractiveInstaller.cs

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ namespace CurseForge.Minecraft.Serverpack.Launcher
1010
{
1111
partial class Program
1212
{
13-
private static async Task<int> InteractiveInstallation()
13+
private static async Task<int> InteractiveInstallation(bool? automaticInstaller, uint? projectId, string fileId)
1414
{
1515
if (!CheckRequiredDependencies())
1616
{
1717
return -1;
1818
}
1919

20+
if (automaticInstaller.HasValue && automaticInstaller.Value)
21+
{
22+
Console.WriteLine("Automatic modpack server installer activated");
23+
Console.WriteLine("ProjectId: {0}, FileId: {1}", projectId, fileId);
24+
}
25+
2026
Console.WriteLine("Activating interactive mode. Please follow the instructions.");
2127
Console.WriteLine("If you want to know other ways to use this, please use the argument --help");
2228
Console.WriteLine();
@@ -51,48 +57,88 @@ private static async Task<int> InteractiveInstallation()
5157

5258
Console.WriteLine();
5359

54-
AnsiConsole.Write(new Rule("Search modpack to install"));
55-
56-
var searchType = AnsiConsole.Prompt(new SelectionPrompt<string>()
57-
.Title("Do you want to search by [orange1 bold]project id[/] or [orange1 bold]project name[/]?")
58-
.AddChoices(new[]
59-
{
60-
"Project Id",
61-
"Project Name"
62-
})
63-
.HighlightStyle(new Style(Color.Orange1)));
64-
65-
Console.WriteLine($"Searching with {searchType}");
66-
6760
GetCfApiInformation(out var cfApiKey, out var cfPartnerId, out var cfContactEmail, out var errors);
6861

6962
if (errors.Count > 0)
7063
{
71-
AnsiConsole.WriteLine("[bold red]Please resolve the errors before continuing.[/]");
64+
AnsiConsole.MarkupLine("[bold red]Please resolve the errors before continuing.[/]");
7265
return -1;
7366
}
7467

7568
using ApiClient cfApiClient = new(cfApiKey, cfPartnerId, cfContactEmail);
7669

77-
try
70+
if (!projectId.HasValue)
7871
{
79-
await cfApiClient.GetGamesAsync();
80-
}
81-
catch
82-
{
83-
Console.WriteLine("Error: Could not contact the CurseForge API, please check your API key");
84-
return -1;
85-
}
72+
AnsiConsole.Write(new Rule("Search modpack to install"));
8673

87-
if (searchType == "Project Id")
88-
{
89-
while (!await HandleProjectIdSearch(cfApiClient))
90-
{ }
74+
var searchType = AnsiConsole.Prompt(new SelectionPrompt<string>()
75+
.Title("Do you want to search by [orange1 bold]project id[/] or [orange1 bold]project name[/]?")
76+
.AddChoices(new[]
77+
{
78+
"Project Id",
79+
"Project Name"
80+
})
81+
.HighlightStyle(new Style(Color.Orange1)));
82+
83+
Console.WriteLine($"Searching with {searchType}");
84+
85+
try
86+
{
87+
await cfApiClient.GetGamesAsync();
88+
}
89+
catch
90+
{
91+
Console.WriteLine("Error: Could not contact the CurseForge API, please check your API key");
92+
return -1;
93+
}
94+
95+
if (searchType == "Project Id")
96+
{
97+
while (!await HandleProjectIdSearch(cfApiClient))
98+
{ }
99+
}
100+
else
101+
{
102+
while (!await HandleProjectSearch(cfApiClient))
103+
{ }
104+
}
91105
}
92106
else
93107
{
94-
while (!await HandleProjectSearch(cfApiClient))
95-
{ }
108+
var _selectedMod = await cfApiClient.GetModAsync(projectId.Value);
109+
if (_selectedMod?.Data == null)
110+
{
111+
Console.Write($"Error: Project {projectId} does not exist");
112+
return -1;
113+
}
114+
115+
selectedMod = _selectedMod.Data;
116+
117+
if (fileId == "latest")
118+
{
119+
var versions = await cfApiClient.GetModFilesAsync(selectedMod.Id);
120+
var validVersions = versions.Data.Where(v => v.FileStatus == APIClient.Models.Files.FileStatus.Approved);
121+
122+
var latestVersion = validVersions.OrderByDescending(c => c.FileDate).First();
123+
124+
selectedVersion = latestVersion;
125+
}
126+
else
127+
{
128+
if (!uint.TryParse(fileId, out var _fileId))
129+
{
130+
Console.WriteLine("Error: Use either \"latest\" or a file id for the version");
131+
return -1;
132+
}
133+
var _selectedFile = await cfApiClient.GetModFileAsync(projectId.Value, _fileId);
134+
if (_selectedFile?.Data == null)
135+
{
136+
Console.Write($"Error: File {fileId} does not exist");
137+
return -1;
138+
}
139+
140+
selectedVersion = _selectedFile.Data;
141+
}
96142
}
97143

98144
if (selectedMod == null)
@@ -101,8 +147,11 @@ private static async Task<int> InteractiveInstallation()
101147
return -1;
102148
}
103149

104-
while (!await HandleProjectVersionSearch(cfApiClient, selectedMod))
105-
{ }
150+
if (selectedVersion == null)
151+
{
152+
while (!await HandleProjectVersionSearch(cfApiClient, selectedMod))
153+
{ }
154+
}
106155

107156
if (selectedVersion == null)
108157
{
@@ -116,13 +165,9 @@ private static async Task<int> InteractiveInstallation()
116165
return 1;
117166
}
118167

119-
var javaArgs = string.Empty;
120168
var startServer = AnsiConsole.Confirm("Do you want to start the server directly?");
121169

122-
if (startServer)
123-
{
124-
javaArgs = AnsiConsole.Ask<string>("Do you want any [orange1 bold]java arguments[/] for the server?");
125-
}
170+
var javaArgs = AnsiConsole.Ask<string>("Do you want any [orange1 bold]java arguments[/] for the server?", "-Xms4G -Xmx4G");
126171

127172
await InstallServer(selectedMod.Id, selectedVersion.Id, serverPath, javaArgs, startServer);
128173

@@ -178,7 +223,7 @@ private static async Task<bool> HandleProjectSearch(ApiClient cfApiClient)
178223

179224
if (modResults.Pagination.TotalCount > modResults.Pagination.ResultCount)
180225
{
181-
int index = modResults.Pagination.Index;
226+
uint index = modResults.Pagination.Index;
182227
while (modsFound.Count < modResults.Pagination.TotalCount)
183228
{
184229
ctx.Status($"Fetching more results ({modResults.Pagination.PageSize * (index + 1)} / {modResults.Pagination.TotalCount})");
@@ -216,7 +261,7 @@ private static async Task<bool> HandleProjectSearch(ApiClient cfApiClient)
216261
private static async Task<bool> HandleProjectIdSearch(ApiClient cfApiClient)
217262
{
218263
var projectId = AnsiConsole.Prompt(
219-
new TextPrompt<int>(
264+
new TextPrompt<uint>(
220265
"Enter [orange1 bold]Project Id[/] of the modpack"
221266
).ValidationErrorMessage("Please enter a valid [orange1 bold]Project Id[/] for a modpack from CurseForge")
222267
.Validate(l => l > 0 ? ValidationResult.Success() : ValidationResult.Error("[orange1 bold]Project Ids[/] cannot be negative"))

CurseForge.Minecraft.Serverpack.Launcher/Program.cs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.IO;
55
using System.IO.Compression;
66
using System.Linq;
7-
using System.Net;
87
using System.Net.Http;
98
using System.Net.Http.Json;
109
using System.Text.Json;
@@ -34,15 +33,50 @@ private static async Task<int> Main(params string[] args)
3433
var command = SetupCommand();
3534
if (args.Length == 0)
3635
{
37-
await command.InvokeAsync("interactive");
38-
Console.ReadKey();
36+
var automaticInstall = await CheckProcessNameForAutomaticInstall(command);
37+
if (!automaticInstall)
38+
{
39+
await command.InvokeAsync("interactive");
40+
Console.ReadKey();
3941

40-
return 0;
42+
return 0;
43+
}
44+
else
45+
{
46+
Console.ReadKey();
47+
return 0;
48+
}
4149
}
4250

4351
return await command.InvokeAsync(args);
4452
}
4553

54+
private async static Task<bool> CheckProcessNameForAutomaticInstall(RootCommand command)
55+
{
56+
var currentProcess = Process.GetCurrentProcess().ProcessName;
57+
58+
Console.WriteLine(currentProcess);
59+
60+
if (currentProcess.Equals("cf-mc-server", StringComparison.InvariantCultureIgnoreCase))
61+
{
62+
return false;
63+
}
64+
65+
if (!currentProcess.StartsWith("cf-", StringComparison.InvariantCultureIgnoreCase) || !currentProcess.EndsWith("-server", StringComparison.InvariantCultureIgnoreCase))
66+
{
67+
return false;
68+
}
69+
70+
var processNameArguments = currentProcess.Split('-');
71+
72+
var projectId = processNameArguments[2];
73+
var fileId = processNameArguments[3];
74+
75+
await command.InvokeAsync($"interactive true {projectId} {fileId}");
76+
77+
return true;
78+
}
79+
4680
private static bool TryDirectoryPath(string path)
4781
{
4882
try
@@ -60,7 +94,7 @@ private static bool TryDirectoryPath(string path)
6094
}
6195
}
6296

63-
private static async Task<int> InstallServer(int modId, int fileId, string path, string javaArgs, bool startServer)
97+
private static async Task<int> InstallServer(uint modId, uint fileId, string path, string javaArgs, bool startServer)
6498
{
6599
GetCfApiInformation(out var cfApiKey, out var cfPartnerId, out var cfContactEmail, out var errors);
66100

@@ -118,13 +152,13 @@ private static async Task<int> InstallServer(int modId, int fileId, string path,
118152
if (!modInfo.Data.Categories.Any(c => c.ClassId == 4471))
119153
{
120154
// Not a modpack
121-
AnsiConsole.WriteLine("[bold red]Error: Project is not a modpack, not allowed in current version of server launcher[/]");
155+
AnsiConsole.MarkupLine("[bold red]Error: Project is not a modpack, not allowed in current version of server launcher[/]");
122156
return -1;
123157
}
124158

125159
if (!(modInfo.Data.AllowModDistribution ?? true) || !modInfo.Data.IsAvailable)
126160
{
127-
AnsiConsole.WriteLine("[bold red]The author of this modpack has not made it available for download through third party tools.[/]");
161+
AnsiConsole.MarkupLine("[bold red]The author of this modpack has not made it available for download through third party tools.[/]");
128162
return -1;
129163
}
130164

@@ -134,12 +168,14 @@ private static async Task<int> InstallServer(int modId, int fileId, string path,
134168
var installPath = Path.Combine(path, "installed", modInfo.Data.Slug);
135169
var manifestPath = Path.Combine(installPath, "manifest.json");
136170

137-
if (!File.Exists(dlPath))
171+
if (!File.Exists(dlPath) || modFile.Data.FileLength != (ulong)new FileInfo(dlPath).Length)
138172
{
139-
#pragma warning disable SYSLIB0014 // Type or member is obsolete
140-
using WebClient wc = new();
141-
#pragma warning restore SYSLIB0014 // Type or member is obsolete
142-
await wc.DownloadFileTaskAsync(modFile.Data.DownloadUrl, dlPath);
173+
// Removes the file, if we have a unfinished download (or if the size differs)
174+
File.Delete(dlPath);
175+
176+
using HttpClient wc = new();
177+
var dlBytes = await wc.GetByteArrayAsync(modFile.Data.DownloadUrl);
178+
await File.WriteAllBytesAsync(dlPath, dlBytes);
143179
}
144180

145181
if (!Directory.Exists(installPath))

CurseForge.Minecraft.Serverpack.Launcher/Properties/launchSettings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
"Specific modpack": {
88
"commandName": "Project",
99
"commandLineArgs": "477455 3295539 \"c:\\mc-server\""
10+
},
11+
"No arguments": {
12+
"commandName": "Project"
13+
},
14+
"Automatic installer": {
15+
"commandName": "Project",
16+
"commandLineArgs": "interactive true 542763 latest"
1017
}
1118
}
1219
}

0 commit comments

Comments
 (0)