ForgeFix и новые фичи в интерфейсе
This commit is contained in:
@@ -210,6 +210,10 @@ public class Bootstrap {
|
|||||||
if (ui != null) ui.setProgress(current, total);
|
if (ui != null) ui.setProgress(current, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void setSpeedText(String text) {
|
||||||
|
if (ui != null) ui.setSpeedText(text);
|
||||||
|
}
|
||||||
|
|
||||||
private static void setVersionInfo(String localVer, String serverVer) {
|
private static void setVersionInfo(String localVer, String serverVer) {
|
||||||
if (ui != null) ui.setVersionInfo(localVer, serverVer);
|
if (ui != null) ui.setVersionInfo(localVer, serverVer);
|
||||||
}
|
}
|
||||||
@@ -467,13 +471,25 @@ public class Bootstrap {
|
|||||||
|
|
||||||
if (downloaded - lastUpdate > 1024 || downloaded == expectedSize) {
|
if (downloaded - lastUpdate > 1024 || downloaded == expectedSize) {
|
||||||
long elapsed = System.currentTimeMillis() - startTime;
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
double speed = downloaded / 1024.0 / 1024.0 / (elapsed / 1000.0 + 0.001);
|
double speedBps = elapsed > 0 ? downloaded * 1000.0 / elapsed : 0;
|
||||||
|
double speed = speedBps / 1024.0 / 1024.0;
|
||||||
double downloadedMB = downloaded / 1024.0 / 1024.0;
|
double downloadedMB = downloaded / 1024.0 / 1024.0;
|
||||||
double totalMB = expectedSize / 1024.0 / 1024.0;
|
double totalMB = expectedSize / 1024.0 / 1024.0;
|
||||||
|
String etaStr = "";
|
||||||
String progressStr = String.format("%.1f/%.1f MB (%.1f MB/s)", downloadedMB, totalMB, speed);
|
if (speedBps > 0) {
|
||||||
|
long etaSec = (long)((expectedSize - downloaded) / speedBps);
|
||||||
|
if (etaSec > 3600) {
|
||||||
|
etaStr = String.format(" ETA: %dh %dm", etaSec / 3600, (etaSec % 3600) / 60);
|
||||||
|
} else if (etaSec > 60) {
|
||||||
|
etaStr = String.format(" ETA: %dm %ds", etaSec / 60, etaSec % 60);
|
||||||
|
} else if (etaSec > 0) {
|
||||||
|
etaStr = " ETA: " + etaSec + "s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String progressStr = String.format("%.1f/%.1f MB (%.1f MB/s%s)", downloadedMB, totalMB, speed, etaStr);
|
||||||
log(progressStr);
|
log(progressStr);
|
||||||
setProgress((int) downloaded, (int) Math.max(expectedSize, 1));
|
setProgress((int) downloaded, (int) Math.max(expectedSize, 1));
|
||||||
|
setSpeedText(String.format("%.1f / %.1f MB \u2022 %.1f MB/s", downloadedMB, totalMB, speed));
|
||||||
lastUpdate = downloaded;
|
lastUpdate = downloaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,14 +714,16 @@ public class Bootstrap {
|
|||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
int pct = (int) ((long) current * 100 / total);
|
int pct = (int) ((long) current * 100 / total);
|
||||||
progressBar.setValue(Math.min(pct, 100));
|
progressBar.setValue(Math.min(pct, 100));
|
||||||
speedLabel.setText(String.format("%.1f / %.1f MB",
|
|
||||||
current / 1024.0 / 1024.0, total / 1024.0 / 1024.0));
|
|
||||||
} else {
|
} else {
|
||||||
progressBar.setIndeterminate(true);
|
progressBar.setIndeterminate(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setSpeedText(final String text) {
|
||||||
|
SwingUtilities.invokeLater(() -> speedLabel.setText(text));
|
||||||
|
}
|
||||||
|
|
||||||
void setVersionInfo(final String local, final String server) {
|
void setVersionInfo(final String local, final String server) {
|
||||||
SwingUtilities.invokeLater(() ->
|
SwingUtilities.invokeLater(() ->
|
||||||
versionLabel.setText("v" + local + " \u2192 v" + server));
|
versionLabel.setText("v" + local + " \u2192 v" + server));
|
||||||
|
|||||||
+10
-10
@@ -61,31 +61,31 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-web</artifactId>
|
<artifactId>javafx-web</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-graphics</artifactId>
|
<artifactId>javafx-graphics</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-base</artifactId>
|
<artifactId>javafx-base</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-media</artifactId>
|
<artifactId>javafx-media</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -226,27 +226,27 @@
|
|||||||
<!-- Копируем JavaFX модули в lib/javafx -->
|
<!-- Копируем JavaFX модули в lib/javafx -->
|
||||||
<mkdir dir="../../server/builds/lib/javafx"/>
|
<mkdir dir="../../server/builds/lib/javafx"/>
|
||||||
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-controls/21">
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-controls/23">
|
||||||
<include name="*win.jar"/>
|
<include name="*win.jar"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-graphics/21">
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-graphics/23">
|
||||||
<include name="*win.jar"/>
|
<include name="*win.jar"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-base/21">
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-base/23">
|
||||||
<include name="*win.jar"/>
|
<include name="*win.jar"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-web/21">
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-web/23">
|
||||||
<include name="*win.jar"/>
|
<include name="*win.jar"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-media/21">
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-media/23">
|
||||||
<include name="*win.jar"/>
|
<include name="*win.jar"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
|
|||||||
@@ -96,41 +96,7 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void mainLoop() throws Exception {
|
private static void mainLoop() throws Exception {
|
||||||
if (Config.isZernMCBuild()) {
|
globalFlow();
|
||||||
zernMCFlow();
|
|
||||||
} else {
|
|
||||||
globalFlow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void zernMCFlow() throws Exception {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Checking connection to ZernMC server..."));
|
|
||||||
try {
|
|
||||||
String response = ZHttpClient.get("/health");
|
|
||||||
System.out.println(ZAnsi.brightGreen("✓ Server is available"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("✗ Could not connect to ZernMC server"));
|
|
||||||
System.out.println(ZAnsi.white("Error: " + e.getMessage()));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean sessionRestored = AuthManager.loadSavedSession();
|
|
||||||
if (!sessionRestored) {
|
|
||||||
LoginMenu loginMenu = new LoginMenu();
|
|
||||||
boolean loggedIn = loginMenu.show();
|
|
||||||
if (!loggedIn) {
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Welcome back, " + AuthManager.getUsername() + "!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchMenu launchMenu = new LaunchMenu();
|
|
||||||
launchMenu.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void globalFlow() throws Exception {
|
private static void globalFlow() throws Exception {
|
||||||
|
|||||||
@@ -26,51 +26,7 @@ import java.util.stream.Collectors;
|
|||||||
public class LaunchMenu {
|
public class LaunchMenu {
|
||||||
|
|
||||||
public void show() throws Exception {
|
public void show() throws Exception {
|
||||||
if (Config.isZernMCBuild()) {
|
showGlobal();
|
||||||
showZernMCOnly();
|
|
||||||
} else {
|
|
||||||
showGlobal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showZernMCOnly() throws Exception {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
|
|
||||||
System.out.println(ZAnsi.cyan("Server packs only"));
|
|
||||||
|
|
||||||
if (!awaitActivePass()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PackDownloader tempDownloader = new PackDownloader(null);
|
|
||||||
List<ServerPack> availablePacks = tempDownloader.getAvailablePacks();
|
|
||||||
|
|
||||||
if (availablePacks.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.yellow("No packs available on the server."));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> options = availablePacks.stream()
|
|
||||||
.map(p -> String.format("%s [%s + %s v%d] - %d files",
|
|
||||||
p.getName(),
|
|
||||||
p.getMinecraftVersion(),
|
|
||||||
p.getLoaderType(),
|
|
||||||
p.getVersion(),
|
|
||||||
p.getFilesCount()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
options.add("Back to main menu");
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Select a pack", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == options.size() - 1) return;
|
|
||||||
|
|
||||||
ServerPack selected = availablePacks.get(choice);
|
|
||||||
installAndRunServerPack(selected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean awaitActivePass() throws Exception {
|
private boolean awaitActivePass() throws Exception {
|
||||||
@@ -130,43 +86,6 @@ public class LaunchMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void installAndRunServerPack(ServerPack selected) throws Exception {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("Installing pack: " + selected.getName()));
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.white(" Minecraft: ") + selected.getMinecraftVersion());
|
|
||||||
System.out.println(ZAnsi.white(" Loader: ") + selected.getLoaderType() +
|
|
||||||
(selected.getLoaderVersion() != null ? " " + selected.getLoaderVersion() : ""));
|
|
||||||
System.out.println(ZAnsi.white(" Version: v") + selected.getVersion());
|
|
||||||
System.out.println(ZAnsi.white(" Files: ") + selected.getFilesCount());
|
|
||||||
|
|
||||||
String localName = askPackName();
|
|
||||||
if (localName == null) return;
|
|
||||||
|
|
||||||
if (InstanceManager.getInstance(localName) != null) {
|
|
||||||
System.out.println(ZAnsi.brightRed("A pack with this name already exists!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InstanceManager.createInstanceFolder(localName);
|
|
||||||
Instance newInstance = InstanceManager.getInstance(localName);
|
|
||||||
|
|
||||||
PackDownloader packDownloader = new PackDownloader(newInstance);
|
|
||||||
boolean success = packDownloader.installOrUpdatePack(selected.getName(), selected);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
System.out.println(ZAnsi.brightRed("\n[FAIL] Could not install the pack."));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\n[OK] Pack '" + localName + "' installed successfully!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
|
|
||||||
launchExistingInstance(newInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showGlobal() throws Exception {
|
private void showGlobal() throws Exception {
|
||||||
while (true) {
|
while (true) {
|
||||||
ConsoleUtils.clearScreen();
|
ConsoleUtils.clearScreen();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class Instance {
|
|||||||
private int serverVersion; // версия сборки на сервере
|
private int serverVersion; // версия сборки на сервере
|
||||||
private String serverPackName; // имя пака на сервере
|
private String serverPackName; // имя пака на сервере
|
||||||
private String fabricVersionId;
|
private String fabricVersionId;
|
||||||
|
private String presetId;
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
@@ -88,6 +89,15 @@ public class Instance {
|
|||||||
saveMetadata();
|
saveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPresetId() {
|
||||||
|
return presetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPresetId(String presetId) {
|
||||||
|
this.presetId = presetId;
|
||||||
|
saveMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
public String getServerPackName() {
|
public String getServerPackName() {
|
||||||
return serverPackName;
|
return serverPackName;
|
||||||
}
|
}
|
||||||
@@ -130,6 +140,7 @@ public class Instance {
|
|||||||
this.isServerPack = meta.isServerPack;
|
this.isServerPack = meta.isServerPack;
|
||||||
this.serverVersion = meta.serverVersion;
|
this.serverVersion = meta.serverVersion;
|
||||||
this.serverPackName = meta.serverPackName;
|
this.serverPackName = meta.serverPackName;
|
||||||
|
this.presetId = meta.presetId;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("[Instance] Failed to load metadata for " + name + ": " + e.getMessage());
|
System.err.println("[Instance] Failed to load metadata for " + name + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -139,7 +150,7 @@ public class Instance {
|
|||||||
Path metaFile = path.resolve("instance.json");
|
Path metaFile = path.resolve("instance.json");
|
||||||
InstanceMeta meta = new InstanceMeta(
|
InstanceMeta meta = new InstanceMeta(
|
||||||
minecraftVersion, loaderType, loaderVersion, assetIndex,
|
minecraftVersion, loaderType, loaderVersion, assetIndex,
|
||||||
isServerPack, serverVersion, serverPackName
|
isServerPack, serverVersion, serverPackName, presetId
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
Files.writeString(metaFile, GSON.toJson(meta));
|
Files.writeString(metaFile, GSON.toJson(meta));
|
||||||
@@ -156,12 +167,13 @@ public class Instance {
|
|||||||
boolean isServerPack = false;
|
boolean isServerPack = false;
|
||||||
int serverVersion = 0;
|
int serverVersion = 0;
|
||||||
String serverPackName;
|
String serverPackName;
|
||||||
|
String presetId;
|
||||||
|
|
||||||
|
|
||||||
public InstanceMeta(String minecraftVersion, String loaderType,
|
public InstanceMeta(String minecraftVersion, String loaderType,
|
||||||
String loaderVersion, String assetIndex,
|
String loaderVersion, String assetIndex,
|
||||||
boolean isServerPack, int serverVersion,
|
boolean isServerPack, int serverVersion,
|
||||||
String serverPackName) {
|
String serverPackName, String presetId) {
|
||||||
this.minecraftVersion = minecraftVersion;
|
this.minecraftVersion = minecraftVersion;
|
||||||
this.loaderType = loaderType;
|
this.loaderType = loaderType;
|
||||||
this.loaderVersion = loaderVersion;
|
this.loaderVersion = loaderVersion;
|
||||||
@@ -169,6 +181,7 @@ public class Instance {
|
|||||||
this.isServerPack = isServerPack;
|
this.isServerPack = isServerPack;
|
||||||
this.serverVersion = serverVersion;
|
this.serverVersion = serverVersion;
|
||||||
this.serverPackName = serverPackName;
|
this.serverPackName = serverPackName;
|
||||||
|
this.presetId = presetId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ import java.io.InputStreamReader;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
public class MinecraftLib {
|
public class MinecraftLib {
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ public class MinecraftLib {
|
|||||||
public void launch(LaunchOptions options) throws Exception {
|
public void launch(LaunchOptions options) throws Exception {
|
||||||
System.out.println(ZAnsi.brightGreen("Launching pack: " + instance.getName()));
|
System.out.println(ZAnsi.brightGreen("Launching pack: " + instance.getName()));
|
||||||
cleanupOldLoaders();
|
cleanupOldLoaders();
|
||||||
|
validateJarFiles();
|
||||||
|
|
||||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||||
List<String> command = builder.build(options);
|
List<String> command = builder.build(options);
|
||||||
@@ -224,6 +227,40 @@ public class MinecraftLib {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void validateJarFiles() {
|
||||||
|
Path[] dirs = {
|
||||||
|
instance.getPath().resolve("mods"),
|
||||||
|
instance.getPath().resolve("libraries")
|
||||||
|
};
|
||||||
|
for (Path dir : dirs) {
|
||||||
|
if (!Files.exists(dir)) continue;
|
||||||
|
try (var stream = Files.walk(dir)) {
|
||||||
|
stream.filter(p -> p.toString().endsWith(".jar"))
|
||||||
|
.filter(p -> !isValidJar(p))
|
||||||
|
.forEach(p -> {
|
||||||
|
try {
|
||||||
|
System.out.println(ZAnsi.yellow(" Removing corrupt JAR: " + instance.getPath().relativize(p)));
|
||||||
|
Files.delete(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println(ZAnsi.red(" Failed to delete: " + p.getFileName()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println(ZAnsi.yellow(" Error scanning for corrupt JARs: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidJar(Path path) {
|
||||||
|
try (ZipFile zf = new ZipFile(path.toFile())) {
|
||||||
|
return true;
|
||||||
|
} catch (ZipException e) {
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Instance getInstance() {
|
public Instance getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|||||||
+252
-67
@@ -14,17 +14,15 @@ import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class PackDownloader {
|
public class PackDownloader {
|
||||||
|
|
||||||
@@ -33,9 +31,10 @@ public class PackDownloader {
|
|||||||
void onProgress(String label, int percent, String stageName, int stageIndex, int stageCount);
|
void onProgress(String label, int percent, String stageName, int stageIndex, int stageCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String USER_FILES_FILE = ".user-files.json";
|
||||||
|
|
||||||
private final Instance instance;
|
private final Instance instance;
|
||||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
|
||||||
private ProgressCallback progressCallback;
|
private ProgressCallback progressCallback;
|
||||||
//private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
//private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||||
|
|
||||||
@@ -157,65 +156,89 @@ public class PackDownloader {
|
|||||||
* Install or update a pack from the server
|
* Install or update a pack from the server
|
||||||
*/
|
*/
|
||||||
public boolean installOrUpdatePack(String packName, ServerPack serverPack) throws Exception {
|
public boolean installOrUpdatePack(String packName, ServerPack serverPack) throws Exception {
|
||||||
LauncherLogger.info("Installing pack " + packName + " from server...");
|
return installOrUpdatePack(packName, serverPack, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean installOrUpdatePack(String packName, ServerPack serverPack, String preset) throws Exception {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: START packName=" + packName + " mc=" + serverPack.getMinecraftVersion() + " loader=" + serverPack.getLoaderType() + " preset=" + (preset != null ? preset : "none"));
|
||||||
reportProgress("Fetching manifest...", 0, "manifest", 0, 5);
|
reportProgress("Fetching manifest...", 0, "manifest", 0, 5);
|
||||||
|
|
||||||
// 1. Get manifest
|
// 1. Get manifest
|
||||||
PackManifest manifest = getPackManifest(packName);
|
PackManifest manifest = getPackManifest(packName);
|
||||||
|
LauncherLogger.info("installOrUpdatePack: manifest loaded version=" + manifest.getVersion() + " mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderType() + " files=" + (manifest.files == null ? "null" : manifest.files.size()));
|
||||||
|
|
||||||
// 2. First install Minecraft + Loader via MinecraftLib
|
// 2. First install Minecraft + Loader via MinecraftLib
|
||||||
MinecraftLib lib = new MinecraftLib(instance);
|
MinecraftLib lib = new MinecraftLib(instance);
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Installing Minecraft " + manifest.getMinecraftVersion() + "..."));
|
LauncherLogger.info("installOrUpdatePack: checking Minecraft install. currentMc=" + instance.getMinecraftVersion() + " targetMc=" + manifest.getMinecraftVersion());
|
||||||
|
|
||||||
boolean needsMinecraftInstall = instance.getMinecraftVersion() == null ||
|
boolean needsMinecraftInstall = instance.getMinecraftVersion() == null ||
|
||||||
!instance.getMinecraftVersion().equals(manifest.getMinecraftVersion());
|
!instance.getMinecraftVersion().equals(manifest.getMinecraftVersion());
|
||||||
|
|
||||||
if (needsMinecraftInstall) {
|
if (needsMinecraftInstall) {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: needs Minecraft install. loader=" + manifest.getLoaderType() + " loaderVer=" + manifest.getLoaderVersion());
|
||||||
if ("fabric".equalsIgnoreCase(manifest.getLoaderType())) {
|
if ("fabric".equalsIgnoreCase(manifest.getLoaderType())) {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: installing Fabric mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion());
|
||||||
boolean success = lib.installFabric(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
boolean success = lib.installFabric(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
||||||
|
LauncherLogger.info("installOrUpdatePack: Fabric install result=" + success);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
System.err.println(ZAnsi.brightRed("Failed to install Fabric"));
|
System.err.println(ZAnsi.brightRed("Failed to install Fabric"));
|
||||||
|
LauncherLogger.error("installOrUpdatePack: Fabric install failed");
|
||||||
reportProgress("Failed to install Fabric", 0, "error", 0, 1);
|
reportProgress("Failed to install Fabric", 0, "error", 0, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if ("neoforge".equalsIgnoreCase(manifest.getLoaderType())) {
|
} else if ("neoforge".equalsIgnoreCase(manifest.getLoaderType())) {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: installing NeoForge mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion());
|
||||||
boolean success = lib.installNeoForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
boolean success = lib.installNeoForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
||||||
|
LauncherLogger.info("installOrUpdatePack: NeoForge install result=" + success);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
System.err.println(ZAnsi.brightRed("Failed to install NeoForge"));
|
System.err.println(ZAnsi.brightRed("Failed to install NeoForge"));
|
||||||
|
LauncherLogger.error("installOrUpdatePack: NeoForge install failed");
|
||||||
reportProgress("Failed to install NeoForge", 0, "error", 0, 1);
|
reportProgress("Failed to install NeoForge", 0, "error", 0, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) {
|
} else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: installing Forge mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion());
|
||||||
boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
||||||
|
LauncherLogger.info("installOrUpdatePack: Forge install result=" + success);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
System.err.println(ZAnsi.brightRed("Failed to install Forge"));
|
System.err.println(ZAnsi.brightRed("Failed to install Forge"));
|
||||||
|
LauncherLogger.error("installOrUpdatePack: Forge install failed");
|
||||||
reportProgress("Failed to install Forge", 0, "error", 0, 1);
|
reportProgress("Failed to install Forge", 0, "error", 0, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: installing Vanilla Minecraft " + manifest.getMinecraftVersion());
|
||||||
boolean success = lib.installMinecraft(manifest.getMinecraftVersion());
|
boolean success = lib.installMinecraft(manifest.getMinecraftVersion());
|
||||||
|
LauncherLogger.info("installOrUpdatePack: Vanilla install result=" + success);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
System.err.println(ZAnsi.brightRed("Failed to install Vanilla Minecraft"));
|
System.err.println(ZAnsi.brightRed("Failed to install Vanilla Minecraft"));
|
||||||
|
LauncherLogger.error("installOrUpdatePack: Vanilla install failed");
|
||||||
reportProgress("Failed to install Vanilla Minecraft", 0, "error", 0, 1);
|
reportProgress("Failed to install Vanilla Minecraft", 0, "error", 0, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LauncherLogger.info("installOrUpdatePack: Minecraft+loader install completed successfully");
|
||||||
} else {
|
} else {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: Minecraft already installed, skipping");
|
||||||
System.out.println(ZAnsi.green("Minecraft already installed, skipping..."));
|
System.out.println(ZAnsi.green("Minecraft already installed, skipping..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
reportProgress("Scanning local files...", 30, "Installing pack files", 4, 5);
|
reportProgress("Scanning local files...", 30, "Installing pack files", 4, 5);
|
||||||
|
|
||||||
// 3. Scan local files only if there are files to download
|
// 3. Scan local files only if there are files to download
|
||||||
|
LauncherLogger.info("installOrUpdatePack: scanning local files...");
|
||||||
Map<String, String> localFiles = scanLocalFiles();
|
Map<String, String> localFiles = scanLocalFiles();
|
||||||
|
LauncherLogger.info("installOrUpdatePack: scanLocalFiles returned " + localFiles.size() + " files. instancePath=" + instance.getPath());
|
||||||
|
|
||||||
// If pack has no files (vanilla/loader only), skip diff
|
// If pack has no files (vanilla/loader only), skip diff
|
||||||
if (manifest.files == null || manifest.files.isEmpty()) {
|
if (manifest.files == null || manifest.files.isEmpty()) {
|
||||||
|
LauncherLogger.info("installOrUpdatePack: no files in manifest, skipping diff");
|
||||||
System.out.println(ZAnsi.green("Pack contains no additional files"));
|
System.out.println(ZAnsi.green("Pack contains no additional files"));
|
||||||
reportProgress("Installing...", 50, "Installing pack files", 4, 5);
|
reportProgress("Installing...", 50, "Installing pack files", 4, 5);
|
||||||
|
|
||||||
// Update instance metadata
|
saveUserFilesSnapshot(localFiles);
|
||||||
|
|
||||||
instance.setServerPack(true);
|
instance.setServerPack(true);
|
||||||
instance.setServerPackName(packName);
|
instance.setServerPackName(packName);
|
||||||
instance.setServerVersion(manifest.getVersion());
|
instance.setServerVersion(manifest.getVersion());
|
||||||
@@ -228,18 +251,44 @@ public class PackDownloader {
|
|||||||
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
|
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
LauncherLogger.info("installOrUpdatePack: manifest has " + manifest.files.size() + " files, proceeding to diff");
|
||||||
|
|
||||||
// 4. Send diff request
|
// 4. Send diff request
|
||||||
System.out.println(ZAnsi.cyan("Checking pack files..."));
|
LauncherLogger.info("installOrUpdatePack: requesting diff from server...");
|
||||||
reportProgress("Checking pack files...", 40, "Installing pack files", 4, 5);
|
reportProgress("Checking pack files...", 40, "Installing pack files", 4, 5);
|
||||||
DiffResponse diff = getDiff(packName, localFiles);
|
DiffResponse diff = null;
|
||||||
|
try {
|
||||||
|
diff = getDiff(packName, localFiles, preset);
|
||||||
|
LauncherLogger.info("installOrUpdatePack: diff received: toDownload=" + diff.getToDownload().size() + " toDelete=" + diff.getToDelete().size() + " version=" + diff.getVersion());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.error("installOrUpdatePack: getDiff failed: " + e.getClass().getName() + " - " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Apply changes
|
// 5. Detect user-modified files
|
||||||
|
Set<String> protectedFiles = getUserModifiedFiles(localFiles);
|
||||||
|
LauncherLogger.info("installOrUpdatePack: user-modified files count=" + protectedFiles.size());
|
||||||
|
|
||||||
|
// 6. Apply changes
|
||||||
|
LauncherLogger.info("installOrUpdatePack: starting applyDiff...");
|
||||||
reportProgress("Downloading pack files...", 50, "Installing pack files", 4, 5);
|
reportProgress("Downloading pack files...", 50, "Installing pack files", 4, 5);
|
||||||
boolean success = applyDiff(diff, packName);
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
success = applyDiff(diff, packName, protectedFiles);
|
||||||
|
LauncherLogger.info("installOrUpdatePack: applyDiff returned=" + success);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.error("installOrUpdatePack: applyDiff threw exception: " + e.getClass().getName() + " - " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 6. Update instance metadata
|
LauncherLogger.info("installOrUpdatePack: saving user files snapshot...");
|
||||||
|
Map<String, String> updatedHashes = scanLocalFiles();
|
||||||
|
saveUserFilesSnapshot(updatedHashes);
|
||||||
|
|
||||||
|
LauncherLogger.info("installOrUpdatePack: updating instance metadata...");
|
||||||
instance.setServerPack(true);
|
instance.setServerPack(true);
|
||||||
instance.setServerPackName(packName);
|
instance.setServerPackName(packName);
|
||||||
instance.setServerVersion(manifest.getVersion());
|
instance.setServerVersion(manifest.getVersion());
|
||||||
@@ -249,6 +298,9 @@ public class PackDownloader {
|
|||||||
instance.setAssetIndex(manifest.getAssetIndex());
|
instance.setAssetIndex(manifest.getAssetIndex());
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
|
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
|
||||||
|
LauncherLogger.info("installOrUpdatePack: SUCCESS");
|
||||||
|
} else {
|
||||||
|
LauncherLogger.error("installOrUpdatePack: FAILED - applyDiff returned false without exception");
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
@@ -271,6 +323,10 @@ public class PackDownloader {
|
|||||||
* Update an existing server pack
|
* Update an existing server pack
|
||||||
*/
|
*/
|
||||||
public boolean updatePack(String packName) throws Exception {
|
public boolean updatePack(String packName) throws Exception {
|
||||||
|
return updatePack(packName, instance.getPresetId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updatePack(String packName, String preset) throws Exception {
|
||||||
System.out.println(ZAnsi.cyan("Checking updates for " + instance.getName() + "..."));
|
System.out.println(ZAnsi.cyan("Checking updates for " + instance.getName() + "..."));
|
||||||
|
|
||||||
PackManifest manifest = getPackManifest(packName);
|
PackManifest manifest = getPackManifest(packName);
|
||||||
@@ -286,13 +342,24 @@ public class PackDownloader {
|
|||||||
// Scan local files
|
// Scan local files
|
||||||
Map<String, String> localFiles = scanLocalFiles();
|
Map<String, String> localFiles = scanLocalFiles();
|
||||||
|
|
||||||
|
// Detect user-modified files (skip overwriting)
|
||||||
|
Set<String> protectedFiles = getUserModifiedFiles(localFiles);
|
||||||
|
if (!protectedFiles.isEmpty()) {
|
||||||
|
LauncherLogger.info("[DEBUG] Found " + protectedFiles.size() + " user-modified files (will not overwrite)");
|
||||||
|
System.out.println(ZAnsi.yellow("Found " + protectedFiles.size() + " user-modified files, they will not be overwritten"));
|
||||||
|
}
|
||||||
|
|
||||||
// Get diff
|
// Get diff
|
||||||
DiffResponse diff = getDiff(packName, localFiles);
|
DiffResponse diff = getDiff(packName, localFiles, preset);
|
||||||
|
|
||||||
// Apply changes
|
// Apply changes
|
||||||
boolean success = applyDiff(diff, packName);
|
boolean success = applyDiff(diff, packName, protectedFiles, preset);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// Save new user files snapshot
|
||||||
|
Map<String, String> updatedHashes = scanLocalFiles();
|
||||||
|
saveUserFilesSnapshot(updatedHashes);
|
||||||
|
|
||||||
instance.setServerVersion(serverVersion);
|
instance.setServerVersion(serverVersion);
|
||||||
System.out.println(ZAnsi.brightGreen("Pack updated to v" + serverVersion));
|
System.out.println(ZAnsi.brightGreen("Pack updated to v" + serverVersion));
|
||||||
}
|
}
|
||||||
@@ -310,7 +377,7 @@ public class PackDownloader {
|
|||||||
// Игнорируемые директории
|
// Игнорируемые директории
|
||||||
Set<String> ignoredDirs = Set.of(
|
Set<String> ignoredDirs = Set.of(
|
||||||
"resourcepacks", "shaderpacks", "saves", "logs",
|
"resourcepacks", "shaderpacks", "saves", "logs",
|
||||||
"crash-reports", "screenshots", "journeymap", "config",
|
"crash-reports", "screenshots", "journeymap",
|
||||||
"natives", "assets", "libraries", "versions", "cache"
|
"natives", "assets", "libraries", "versions", "cache"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -324,7 +391,8 @@ public class PackDownloader {
|
|||||||
Path relative = instancePath.relativize(file);
|
Path relative = instancePath.relativize(file);
|
||||||
String path = relative.toString().replace("\\", "/");
|
String path = relative.toString().replace("\\", "/");
|
||||||
|
|
||||||
// Проверяем, не в игнорируемой ли директории
|
// Skip user files snapshot and ignored directories
|
||||||
|
if (path.equals(USER_FILES_FILE)) return;
|
||||||
for (String ignored : ignoredDirs) {
|
for (String ignored : ignoredDirs) {
|
||||||
if (path.startsWith(ignored + "/") || path.startsWith(ignored + "\\")) {
|
if (path.startsWith(ignored + "/") || path.startsWith(ignored + "\\")) {
|
||||||
return;
|
return;
|
||||||
@@ -346,6 +414,10 @@ public class PackDownloader {
|
|||||||
* Send diff request to server
|
* Send diff request to server
|
||||||
*/
|
*/
|
||||||
private DiffResponse getDiff(String packName, Map<String, String> localFiles) throws Exception {
|
private DiffResponse getDiff(String packName, Map<String, String> localFiles) throws Exception {
|
||||||
|
return getDiff(packName, localFiles, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiffResponse getDiff(String packName, Map<String, String> localFiles, String preset) throws Exception {
|
||||||
String json = gson.toJson(localFiles);
|
String json = gson.toJson(localFiles);
|
||||||
|
|
||||||
// Get auth token
|
// Get auth token
|
||||||
@@ -358,6 +430,10 @@ public class PackDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String url = ZHttpClient.getBaseUrl() + "/pack/" + packName + "/diff";
|
String url = ZHttpClient.getBaseUrl() + "/pack/" + packName + "/diff";
|
||||||
|
if (preset != null && !preset.isEmpty()) {
|
||||||
|
url += "?preset=" + java.net.URLEncoder.encode(preset, "UTF-8");
|
||||||
|
}
|
||||||
|
LauncherLogger.info("getDiff: url=" + url + " bodySize=" + json.length() + " authToken=" + (accessToken != null ? "present" : "null") + " localFilesCount=" + json.length());
|
||||||
|
|
||||||
// Use HttpURLConnection for full control
|
// Use HttpURLConnection for full control
|
||||||
java.net.HttpURLConnection connection = null;
|
java.net.HttpURLConnection connection = null;
|
||||||
@@ -381,6 +457,7 @@ public class PackDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
|
LauncherLogger.info("getDiff: responseCode=" + responseCode + " packName=" + packName);
|
||||||
|
|
||||||
// Read response
|
// Read response
|
||||||
StringBuilder response = new StringBuilder();
|
StringBuilder response = new StringBuilder();
|
||||||
@@ -393,6 +470,7 @@ public class PackDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String responseBody = response.toString();
|
String responseBody = response.toString();
|
||||||
|
LauncherLogger.info("[DEBUG] getDiff response body: " + responseBody.substring(0, Math.min(responseBody.length(), 500)));
|
||||||
|
|
||||||
if (responseCode == 403) {
|
if (responseCode == 403) {
|
||||||
throw new IOException("Active pass required to download packs. Contact the administrator.");
|
throw new IOException("Active pass required to download packs. Contact the administrator.");
|
||||||
@@ -402,7 +480,9 @@ public class PackDownloader {
|
|||||||
throw new IOException("HTTP " + responseCode + ": " + extractErrorFromResponse(responseBody));
|
throw new IOException("HTTP " + responseCode + ": " + extractErrorFromResponse(responseBody));
|
||||||
}
|
}
|
||||||
|
|
||||||
return gson.fromJson(responseBody, DiffResponse.class);
|
DiffResponse result = gson.fromJson(responseBody, DiffResponse.class);
|
||||||
|
LauncherLogger.info("[DEBUG] getDiff parsed: toDownload=" + result.getToDownload().size() + " toDelete=" + result.getToDelete().size());
|
||||||
|
return result;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
@@ -425,9 +505,21 @@ public class PackDownloader {
|
|||||||
* Apply diff (download new files, delete old ones)
|
* Apply diff (download new files, delete old ones)
|
||||||
*/
|
*/
|
||||||
private boolean applyDiff(DiffResponse diff, String packName) {
|
private boolean applyDiff(DiffResponse diff, String packName) {
|
||||||
|
return applyDiff(diff, packName, new HashSet<>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyDiff(DiffResponse diff, String packName, Set<String> protectedFiles) {
|
||||||
|
return applyDiff(diff, packName, protectedFiles, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyDiff(DiffResponse diff, String packName, Set<String> protectedFiles, String preset) {
|
||||||
|
LauncherLogger.info("applyDiff: start. toDownload=" + diff.getToDownload().size() + " toDelete=" + diff.getToDelete().size() + " protected=" + protectedFiles.size());
|
||||||
System.out.println(ZAnsi.cyan("\nApplying changes:"));
|
System.out.println(ZAnsi.cyan("\nApplying changes:"));
|
||||||
System.out.println(" Download: " + diff.getToDownload().size() + " files");
|
System.out.println(" Download: " + diff.getToDownload().size() + " files");
|
||||||
System.out.println(" Delete: " + diff.getToDelete().size() + " files");
|
System.out.println(" Delete: " + diff.getToDelete().size() + " files");
|
||||||
|
if (!protectedFiles.isEmpty()) {
|
||||||
|
System.out.println(ZAnsi.yellow(" Protected: " + protectedFiles.size() + " user-modified files (skipped)"));
|
||||||
|
}
|
||||||
|
|
||||||
// Create directories if needed
|
// Create directories if needed
|
||||||
try {
|
try {
|
||||||
@@ -437,8 +529,12 @@ public class PackDownloader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete files
|
// Delete files (skip protected)
|
||||||
for (String filePath : diff.getToDelete()) {
|
for (String filePath : diff.getToDelete()) {
|
||||||
|
if (protectedFiles.contains(filePath)) {
|
||||||
|
System.out.println(ZAnsi.yellow(" Skipped delete (user-modified): " + filePath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Path fullPath = instance.getPath().resolve(filePath);
|
Path fullPath = instance.getPath().resolve(filePath);
|
||||||
try {
|
try {
|
||||||
if (Files.deleteIfExists(fullPath)) {
|
if (Files.deleteIfExists(fullPath)) {
|
||||||
@@ -449,12 +545,25 @@ public class PackDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download files
|
// Download files (skip protected)
|
||||||
AtomicInteger downloaded = new AtomicInteger(0);
|
AtomicInteger downloaded = new AtomicInteger(0);
|
||||||
int total = diff.getToDownload().size();
|
int total = diff.getToDownload().size();
|
||||||
|
int skipped = 0;
|
||||||
|
long totalBytes = 0;
|
||||||
|
for (FileInfo f : diff.getToDownload()) totalBytes += f.getSize();
|
||||||
|
AtomicLong downloadedBytes = new AtomicLong(0);
|
||||||
|
long downloadStartTime = System.currentTimeMillis();
|
||||||
|
LauncherLogger.info("applyDiff: downloading " + total + " files, total size=" + totalBytes + " bytes");
|
||||||
|
|
||||||
for (FileInfo file : diff.getToDownload()) {
|
for (FileInfo file : diff.getToDownload()) {
|
||||||
String path = file.getPath();
|
String path = file.getPath();
|
||||||
|
|
||||||
|
if (protectedFiles.contains(path)) {
|
||||||
|
skipped++;
|
||||||
|
System.out.println(ZAnsi.yellow(" Skipped (user-modified): " + path));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Path fullPath = instance.getPath().resolve(path);
|
Path fullPath = instance.getPath().resolve(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -462,80 +571,103 @@ public class PackDownloader {
|
|||||||
Files.createDirectories(fullPath.getParent());
|
Files.createDirectories(fullPath.getParent());
|
||||||
|
|
||||||
// Download file
|
// Download file
|
||||||
|
String fileUrl = ZHttpClient.getBaseUrl() + file.getUrl();
|
||||||
|
if (preset != null && !preset.isEmpty() && !file.getUrl().contains("preset=")) {
|
||||||
|
String sep = file.getUrl().contains("?") ? "&" : "?";
|
||||||
|
fileUrl = fileUrl + sep + "preset=" + java.net.URLEncoder.encode(preset, "UTF-8");
|
||||||
|
}
|
||||||
|
LauncherLogger.info("applyDiff: downloading file=" + path + " url=" + fileUrl + " size=" + file.getSize() + " hash=" + file.getHash());
|
||||||
downloadFile(file, fullPath);
|
downloadFile(file, fullPath);
|
||||||
|
|
||||||
// Verify hash
|
// Verify hash
|
||||||
String actualHash = calculateHash(fullPath);
|
String actualHash = calculateHash(fullPath);
|
||||||
if (!actualHash.equals(file.getHash())) {
|
boolean hashMatch = actualHash.equals(file.getHash());
|
||||||
throw new IOException("Hash mismatch! Expected: " + file.getHash() +
|
LauncherLogger.info("applyDiff: downloaded file=" + path + " hashMatch=" + hashMatch + " expected=" + file.getHash() + " got=" + actualHash);
|
||||||
", got: " + actualHash);
|
if (!hashMatch) {
|
||||||
|
LauncherLogger.warn("applyDiff: hash mismatch for " + path + ", retrying once...");
|
||||||
|
downloadFile(file, fullPath);
|
||||||
|
actualHash = calculateHash(fullPath);
|
||||||
|
hashMatch = actualHash.equals(file.getHash());
|
||||||
|
LauncherLogger.info("applyDiff: retry result for " + path + " hashMatch=" + hashMatch);
|
||||||
|
}
|
||||||
|
if (!hashMatch) {
|
||||||
|
LauncherLogger.warn("applyDiff: skipping file " + path + " due to hash mismatch (expected=" + file.getHash() + " got=" + actualHash + ")");
|
||||||
|
System.err.println(ZAnsi.yellow(" Skipped (hash mismatch): " + path));
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloaded.incrementAndGet();
|
downloaded.incrementAndGet();
|
||||||
|
downloadedBytes.addAndGet(file.getSize());
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
ProgressBar.show("Download", downloaded.get(), total, "files");
|
ProgressBar.show("Download", downloaded.get(), total, "files");
|
||||||
int pct = 50 + (downloaded.get() * 45 / total);
|
long elapsed = System.currentTimeMillis() - downloadStartTime;
|
||||||
reportProgress("Downloading " + downloaded.get() + "/" + total + " files...", pct, "Installing pack files", 4, 5);
|
long bytesSoFar = downloadedBytes.get();
|
||||||
|
long remainingBytes = totalBytes - bytesSoFar;
|
||||||
|
int pct = totalBytes > 0 ? (int)(bytesSoFar * 45 / totalBytes) : (downloaded.get() * 45 / total);
|
||||||
|
pct = Math.min(pct, 45);
|
||||||
|
String speedStr = "";
|
||||||
|
String etaStr = "";
|
||||||
|
if (elapsed > 1000) {
|
||||||
|
long speedBps = bytesSoFar * 1000 / elapsed;
|
||||||
|
if (speedBps > 1_000_000) {
|
||||||
|
speedStr = String.format("%.1f MB/s", speedBps / 1_000_000.0);
|
||||||
|
} else if (speedBps > 1_000) {
|
||||||
|
speedStr = String.format("%.0f KB/s", speedBps / 1_000.0);
|
||||||
|
} else {
|
||||||
|
speedStr = speedBps + " B/s";
|
||||||
|
}
|
||||||
|
if (speedBps > 0 && remainingBytes > 0) {
|
||||||
|
long etaSec = remainingBytes / speedBps;
|
||||||
|
if (etaSec > 3600) {
|
||||||
|
etaStr = String.format("%dh %dm", etaSec / 3600, (etaSec % 3600) / 60);
|
||||||
|
} else if (etaSec > 60) {
|
||||||
|
etaStr = String.format("%dm %ds", etaSec / 60, etaSec % 60);
|
||||||
|
} else {
|
||||||
|
etaStr = etaSec + "s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String label = "Downloading " + downloaded.get() + "/" + total + " files";
|
||||||
|
if (!speedStr.isEmpty()) label += " (" + speedStr;
|
||||||
|
if (!etaStr.isEmpty()) label += " ETA: " + etaStr;
|
||||||
|
if (!speedStr.isEmpty()) label += ")";
|
||||||
|
reportProgress(label, 50 + pct, "Installing pack files", 4, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.error("applyDiff: download FAILED for path=" + path + " error=" + e.getClass().getName() + ": " + e.getMessage());
|
||||||
System.err.println("\n" + ZAnsi.red(" Download error " + path + ": " + e.getMessage()));
|
System.err.println("\n" + ZAnsi.red(" Download error " + path + ": " + e.getMessage()));
|
||||||
return false;
|
System.err.println(ZAnsi.yellow(" Skipping file " + path + " due to download error"));
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
ProgressBar.finish("Download");
|
ProgressBar.finish("Download");
|
||||||
}
|
}
|
||||||
|
if (skipped > 0) {
|
||||||
|
System.out.println(ZAnsi.cyan(" Skipped " + skipped + " user-modified files"));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачать один файл с сервера
|
* Скачать один файл с сервера через временный файл для атомарности.
|
||||||
|
* Предотвращает появление битых файлов при обрыве соединения.
|
||||||
*/
|
*/
|
||||||
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
||||||
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
Path tmpFile = destination.resolveSibling(destination.getFileName() + ".tmp");
|
||||||
String accessToken = AuthManager.getAccessToken();
|
try {
|
||||||
|
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
ZHttpClient.downloadFileWithSmartProxy(url, tmpFile);
|
||||||
.uri(java.net.URI.create(url))
|
Files.move(tmpFile, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
.timeout(Duration.ofSeconds(60))
|
} catch (Exception e) {
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
try { Files.deleteIfExists(tmpFile); } catch (Exception ignored) {}
|
||||||
.GET();
|
throw e;
|
||||||
|
|
||||||
if (accessToken != null && !accessToken.equals("0")) {
|
|
||||||
builder.header("Authorization", "Bearer " + accessToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRequest request = builder.build();
|
|
||||||
HttpResponse<InputStream> response = httpClient.send(request,
|
|
||||||
HttpResponse.BodyHandlers.ofInputStream());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Скачиваем с прогрессом
|
|
||||||
try (InputStream in = response.body();
|
|
||||||
FileOutputStream out = new FileOutputStream(destination.toFile())) {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
long totalRead = 0;
|
|
||||||
long fileSize = file.getSize();
|
|
||||||
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
totalRead += bytesRead;
|
|
||||||
|
|
||||||
if (fileSize > 0 && totalRead % 8192 == 0) {
|
|
||||||
ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressBar.clearLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -560,6 +692,59 @@ public class PackDownloader {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================== User Files Protection ======================
|
||||||
|
// Keeps a snapshot of file hashes after each install/update.
|
||||||
|
// On next update, files modified by the user are not overwritten.
|
||||||
|
|
||||||
|
private Path getUserFilesPath() {
|
||||||
|
return instance.getPath().resolve(USER_FILES_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveUserFilesSnapshot(Map<String, String> fileHashes) {
|
||||||
|
try {
|
||||||
|
Path path = getUserFilesPath();
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, gson.toJson(fileHashes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.warn("Failed to save user files snapshot: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> loadUserFilesSnapshot() {
|
||||||
|
Path path = getUserFilesPath();
|
||||||
|
if (!Files.exists(path)) return new HashMap<>();
|
||||||
|
try {
|
||||||
|
String content = Files.readString(path);
|
||||||
|
Map<String, Object> raw = gson.fromJson(content, Map.class);
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
if (raw != null) {
|
||||||
|
for (Map.Entry<String, Object> e : raw.entrySet()) {
|
||||||
|
result.put(e.getKey(), e.getValue() != null ? e.getValue().toString() : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LauncherLogger.warn("Failed to load user files snapshot: " + e.getMessage());
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getUserModifiedFiles(Map<String, String> currentHashes) {
|
||||||
|
Map<String, String> snapshot = loadUserFilesSnapshot();
|
||||||
|
if (snapshot.isEmpty()) return new HashSet<>();
|
||||||
|
|
||||||
|
Set<String> modified = new HashSet<>();
|
||||||
|
for (Map.Entry<String, String> e : currentHashes.entrySet()) {
|
||||||
|
String path = e.getKey();
|
||||||
|
String currentHash = e.getValue();
|
||||||
|
String snapshotHash = snapshot.get(path);
|
||||||
|
if (snapshotHash != null && !snapshotHash.equals(currentHash)) {
|
||||||
|
modified.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
// ====================== Вложенные классы ======================
|
// ====================== Вложенные классы ======================
|
||||||
|
|
||||||
public static class PackManifest {
|
public static class PackManifest {
|
||||||
|
|||||||
+2
-1
@@ -157,7 +157,8 @@ public class ForgeInstaller {
|
|||||||
"java",
|
"java",
|
||||||
"-jar",
|
"-jar",
|
||||||
installerJar.toAbsolutePath().toString(),
|
installerJar.toAbsolutePath().toString(),
|
||||||
"--installClient"
|
"--installClient",
|
||||||
|
instance.getPath().toAbsolutePath().toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add JVM args for increased timeouts
|
// Add JVM args for increased timeouts
|
||||||
|
|||||||
+2
-1
@@ -164,7 +164,8 @@ public class NeoForgeInstaller {
|
|||||||
"java",
|
"java",
|
||||||
"-jar",
|
"-jar",
|
||||||
installerJar.toAbsolutePath().toString(),
|
installerJar.toAbsolutePath().toString(),
|
||||||
"--installClient"
|
"--installClient",
|
||||||
|
instance.getPath().toAbsolutePath().toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
|
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
|
||||||
|
|||||||
+308
-20
@@ -5,15 +5,30 @@ import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
|||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class LaunchCommandBuilder {
|
public class LaunchCommandBuilder {
|
||||||
|
|
||||||
|
private static final List<Path> tempFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
for (Path p : tempFiles) {
|
||||||
|
try { Files.deleteIfExists(p); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private final Instance instance;
|
private final Instance instance;
|
||||||
|
|
||||||
public LaunchCommandBuilder(Instance instance) {
|
public LaunchCommandBuilder(Instance instance) {
|
||||||
@@ -42,13 +57,68 @@ public class LaunchCommandBuilder {
|
|||||||
|
|
||||||
VersionManifest manifest = resolveVersionManifest();
|
VersionManifest manifest = resolveVersionManifest();
|
||||||
|
|
||||||
// For modloaders, always use vanilla classpath with all libraries
|
// For modloaders, use vanilla classpath but read main class from manifest when available
|
||||||
if (isModloader) {
|
if (isModloader) {
|
||||||
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
// For Forge/NeoForge: use recursive scan of libraries/ for classpath,
|
||||||
command.add("-cp");
|
// always use ModLauncher as main class (version.json might have
|
||||||
command.add(buildVanillaClasspath());
|
// BootstrapLauncher which breaks with Java module system)
|
||||||
command.add(getVanillaMainClass());
|
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
|
||||||
command.addAll(getVanillaGameArguments(options));
|
// BootstrapLauncher creates the module layer ModLauncher needs
|
||||||
|
// for ServiceLoader discovery.
|
||||||
|
// Build classpath from manifest (shorter, ordered).
|
||||||
|
// Only JARs explicitly listed in version.json go on the classpath.
|
||||||
|
// Do NOT append JARs from recursive scan — tool JARs like
|
||||||
|
// ForgeAutoRenamingTool that contain shaded ASM packages cause
|
||||||
|
// split-package conflicts with the Java module system when
|
||||||
|
// --add-modules=ALL-MODULE-PATH resolves them as automatic modules.
|
||||||
|
String classpath;
|
||||||
|
if (manifest != null) {
|
||||||
|
classpath = buildClasspathFromManifest(manifest);
|
||||||
|
} else {
|
||||||
|
classpath = "";
|
||||||
|
}
|
||||||
|
if (classpath == null || classpath.isEmpty()) {
|
||||||
|
classpath = buildVanillaClasspath();
|
||||||
|
} else {
|
||||||
|
// Safety net: remove JARs from classpath that are already
|
||||||
|
// on the module path (from manifest JVM args) to prevent
|
||||||
|
// Java module system split-package conflicts.
|
||||||
|
classpath = filterClasspathAgainstModulePath(classpath, manifest.getJvmArguments());
|
||||||
|
}
|
||||||
|
command.add("-cp");
|
||||||
|
command.add(writeClasspathFile(classpath));
|
||||||
|
|
||||||
|
// Add JVM arguments from version.json manifest
|
||||||
|
// Includes -DignoreList, -p (module path), --add-modules, --add-opens,
|
||||||
|
// and -DlibraryDirectory — all required for BootstrapLauncher to
|
||||||
|
// resolve cpw.mods.securejarhandler as a module.
|
||||||
|
if (manifest != null) {
|
||||||
|
Map<String, String> vars = buildVariableMap(options);
|
||||||
|
for (String arg : manifest.getJvmArguments()) {
|
||||||
|
command.add(resolveVariable(arg, vars));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
|
||||||
|
command.add(getVanillaMainClass());
|
||||||
|
command.addAll(getVanillaGameArguments(options));
|
||||||
|
command.addAll(getModloaderLaunchArgs());
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
||||||
|
command.add("-cp");
|
||||||
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
|
String mainClass = null;
|
||||||
|
if (manifest != null) {
|
||||||
|
mainClass = manifest.getMainClass();
|
||||||
|
System.out.println(ZAnsi.cyan(" Main class from manifest: " + mainClass));
|
||||||
|
}
|
||||||
|
if (mainClass == null || mainClass.isEmpty()) {
|
||||||
|
mainClass = getVanillaMainClass();
|
||||||
|
System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass));
|
||||||
|
}
|
||||||
|
command.add(mainClass);
|
||||||
|
command.addAll(getVanillaGameArguments(options));
|
||||||
|
}
|
||||||
} else if (manifest != null) {
|
} else if (manifest != null) {
|
||||||
String classpath = buildClasspathFromManifest(manifest);
|
String classpath = buildClasspathFromManifest(manifest);
|
||||||
|
|
||||||
@@ -56,12 +126,12 @@ public class LaunchCommandBuilder {
|
|||||||
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
|
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
|
||||||
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
|
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(buildVanillaClasspath());
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
command.add(getVanillaMainClass());
|
command.add(getVanillaMainClass());
|
||||||
command.addAll(getVanillaGameArguments(options));
|
command.addAll(getVanillaGameArguments(options));
|
||||||
} else {
|
} else {
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(classpath);
|
command.add(writeClasspathFile(classpath));
|
||||||
|
|
||||||
String mainClass = resolveMainClass(manifest);
|
String mainClass = resolveMainClass(manifest);
|
||||||
command.add(mainClass);
|
command.add(mainClass);
|
||||||
@@ -70,7 +140,7 @@ public class LaunchCommandBuilder {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(buildVanillaClasspath());
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
command.add(getVanillaMainClass());
|
command.add(getVanillaMainClass());
|
||||||
command.addAll(getVanillaGameArguments(options));
|
command.addAll(getVanillaGameArguments(options));
|
||||||
}
|
}
|
||||||
@@ -85,7 +155,12 @@ public class LaunchCommandBuilder {
|
|||||||
String content = Files.readString(versionJson);
|
String content = Files.readString(versionJson);
|
||||||
JSONObject json = new JSONObject(content);
|
JSONObject json = new JSONObject(content);
|
||||||
System.out.println(ZAnsi.green("Found version.json: " + versionJson.getFileName()));
|
System.out.println(ZAnsi.green("Found version.json: " + versionJson.getFileName()));
|
||||||
return new VersionManifest(json);
|
VersionManifest manifest = new VersionManifest(json);
|
||||||
|
manifest.resolveParent(instance.getPath().resolve("versions"));
|
||||||
|
if (manifest.getInheritsFrom() != null) {
|
||||||
|
System.out.println(ZAnsi.cyan(" inheritsFrom: " + manifest.getInheritsFrom()));
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow("version.json not found for " + instance.getName()));
|
System.out.println(ZAnsi.yellow("version.json not found for " + instance.getName()));
|
||||||
System.out.println(ZAnsi.yellow(" loaderType=" + instance.getLoaderType() + " mcVersion=" + instance.getMinecraftVersion() + " loaderVersion=" + instance.getLoaderVersion()));
|
System.out.println(ZAnsi.yellow(" loaderType=" + instance.getLoaderType() + " mcVersion=" + instance.getMinecraftVersion() + " loaderVersion=" + instance.getLoaderVersion()));
|
||||||
@@ -212,13 +287,107 @@ public class LaunchCommandBuilder {
|
|||||||
if ("fabric".equals(loaderType)) {
|
if ("fabric".equals(loaderType)) {
|
||||||
return "net.fabricmc.loader.impl.launch.knot.KnotClient";
|
return "net.fabricmc.loader.impl.launch.knot.KnotClient";
|
||||||
} else if ("forge".equals(loaderType)) {
|
} else if ("forge".equals(loaderType)) {
|
||||||
return "net.minecraftforge.client.main.ForgeClient";
|
return "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||||
} else if ("neoforge".equals(loaderType)) {
|
} else if ("neoforge".equals(loaderType)) {
|
||||||
return "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
return "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||||
}
|
}
|
||||||
return "net.minecraft.client.main.Main";
|
return "net.minecraft.client.main.Main";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> getModloaderLaunchArgs() {
|
||||||
|
List<String> args = new ArrayList<>();
|
||||||
|
String loaderType = instance.getLoaderType().toLowerCase();
|
||||||
|
String mcVer = instance.getMinecraftVersion();
|
||||||
|
String loaderVer = instance.getLoaderVersion();
|
||||||
|
|
||||||
|
if ("forge".equals(loaderType)) {
|
||||||
|
args.add("--launchTarget");
|
||||||
|
args.add("forgeclient");
|
||||||
|
if (loaderVer != null && !loaderVer.isEmpty()) {
|
||||||
|
args.add("--fml.forgeVersion");
|
||||||
|
args.add(loaderVer);
|
||||||
|
}
|
||||||
|
if (mcVer != null && !mcVer.isEmpty()) {
|
||||||
|
args.add("--fml.mcVersion");
|
||||||
|
args.add(mcVer);
|
||||||
|
}
|
||||||
|
args.add("--fml.forgeGroup");
|
||||||
|
args.add("net.minecraftforge");
|
||||||
|
// Read mcpVersion from version.json if possible, fallback to empty
|
||||||
|
String mcpVer = getMcpVersion(mcVer);
|
||||||
|
if (mcpVer != null && !mcpVer.isEmpty()) {
|
||||||
|
args.add("--fml.mcpVersion");
|
||||||
|
args.add(mcpVer);
|
||||||
|
}
|
||||||
|
} else if ("neoforge".equals(loaderType)) {
|
||||||
|
args.add("--launchTarget");
|
||||||
|
args.add("neoforgeclient");
|
||||||
|
if (loaderVer != null && !loaderVer.isEmpty()) {
|
||||||
|
args.add("--fml.neoForgeVersion");
|
||||||
|
args.add(loaderVer);
|
||||||
|
}
|
||||||
|
if (mcVer != null && !mcVer.isEmpty()) {
|
||||||
|
args.add("--fml.mcVersion");
|
||||||
|
args.add(mcVer);
|
||||||
|
}
|
||||||
|
args.add("--fml.neoForgeGroup");
|
||||||
|
args.add("net.neoforged");
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMcpVersion(String mcVersion) {
|
||||||
|
if (mcVersion == null) return null;
|
||||||
|
Path versionsDir = instance.getPath().resolve("versions");
|
||||||
|
String loaderType = instance.getLoaderType().toLowerCase();
|
||||||
|
String loaderVer = instance.getLoaderVersion();
|
||||||
|
// Try the same candidate paths as findVersionJson()
|
||||||
|
String[] candidates = {
|
||||||
|
getVersionId(),
|
||||||
|
mcVersion + "-" + loaderType + "-" + loaderVer,
|
||||||
|
loaderType + "-" + loaderVer,
|
||||||
|
mcVersion + "-" + loaderVer,
|
||||||
|
mcVersion
|
||||||
|
};
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
try {
|
||||||
|
Path jsonPath = versionsDir.resolve(candidate).resolve(candidate + ".json");
|
||||||
|
if (Files.exists(jsonPath)) {
|
||||||
|
String content = Files.readString(jsonPath);
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
if (json.has("+mcpVersion")) {
|
||||||
|
return json.getString("+mcpVersion");
|
||||||
|
}
|
||||||
|
if (json.has("mcpVersion")) {
|
||||||
|
return json.getString("mcpVersion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
// Fallback: known MCP versions for common MC releases
|
||||||
|
switch (mcVersion) {
|
||||||
|
case "1.21": return "20240808.143909";
|
||||||
|
case "1.20.6": return "20240429.145301";
|
||||||
|
case "1.20.4": return "20231214.122116";
|
||||||
|
case "1.20.3": return "20231214.122116";
|
||||||
|
case "1.20.2": return "20231119.165538";
|
||||||
|
case "1.20.1": return "20231207.220116";
|
||||||
|
case "1.20": return "20231013.142340";
|
||||||
|
case "1.19.4": return "20230316.153408";
|
||||||
|
case "1.19.3": return "20221207.173059";
|
||||||
|
case "1.19.2": return "20221124.190222";
|
||||||
|
case "1.19.1": return "20220810.141647";
|
||||||
|
case "1.19": return "20220617.144144";
|
||||||
|
case "1.18.2": return "20220404.170452";
|
||||||
|
case "1.18.1": return "20220116.232031";
|
||||||
|
case "1.18": return "20211214.190725";
|
||||||
|
case "1.17.1": return "20210727.130541";
|
||||||
|
case "1.17": return "20210701.125435";
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> resolveGameArguments(VersionManifest manifest, LaunchOptions options) {
|
private List<String> resolveGameArguments(VersionManifest manifest, LaunchOptions options) {
|
||||||
List<String> args = new ArrayList<>();
|
List<String> args = new ArrayList<>();
|
||||||
Map<String, String> vars = buildVariableMap(options);
|
Map<String, String> vars = buildVariableMap(options);
|
||||||
@@ -321,19 +490,53 @@ public class LaunchCommandBuilder {
|
|||||||
List<String> paths = new ArrayList<>();
|
List<String> paths = new ArrayList<>();
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
Path librariesDir = instance.getPath().resolve("libraries");
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getLibraries().size() + " libraries in manifest"));
|
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest (including inherited)"));
|
||||||
|
|
||||||
for (VersionManifest.Library lib : manifest.getLibraries()) {
|
String loaderType = instance.getLoaderType().toLowerCase();
|
||||||
Path libPath = librariesDir.resolve(lib.artifactPath);
|
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
|
||||||
if (Files.exists(libPath)) {
|
|
||||||
paths.add(libPath.toAbsolutePath().toString());
|
for (VersionManifest.Library lib : manifest.getAllLibraries()) {
|
||||||
|
// For Forge/NeoForge: skip Minecraft client libraries — they're loaded
|
||||||
|
// through Forge's own module system (securejarhandler + module path).
|
||||||
|
// Having them on the classpath creates automatic modules that conflict
|
||||||
|
// with Forge's own module layer (split-package ResolutionException).
|
||||||
|
if (isForgeLike && (lib.name.startsWith("net.minecraft:client") || lib.name.contains("minecraft"))) {
|
||||||
|
System.out.println(ZAnsi.cyan(" Skipping Minecraft lib: " + lib.name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Path libPath = lib.artifactPath != null ? librariesDir.resolve(lib.artifactPath) : null;
|
||||||
|
if (libPath != null && Files.exists(libPath)) {
|
||||||
|
if (isValidJar(libPath)) {
|
||||||
|
paths.add(libPath.toAbsolutePath().toString());
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||||
|
Files.delete(libPath);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
String mavenPath = mavenToPath(lib.name);
|
String mavenPath = mavenToPath(lib.name);
|
||||||
Path fallbackPath = librariesDir.resolve(mavenPath);
|
Path fallbackPath = librariesDir.resolve(mavenPath);
|
||||||
if (Files.exists(fallbackPath)) {
|
if (Files.exists(fallbackPath)) {
|
||||||
paths.add(fallbackPath.toAbsolutePath().toString());
|
if (isValidJar(fallbackPath)) {
|
||||||
|
paths.add(fallbackPath.toAbsolutePath().toString());
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||||
|
Files.delete(fallbackPath);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" Library not found: " + lib.name));
|
// Last resort: scan libraries dir for a JAR matching this artifact
|
||||||
|
String artifactName = lib.name.split(":")[1];
|
||||||
|
Path found = scanForJar(librariesDir, artifactName);
|
||||||
|
if (found != null) {
|
||||||
|
if (isValidJar(found)) {
|
||||||
|
System.out.println(ZAnsi.green(" Found by scan: " + lib.name + " → " + librariesDir.relativize(found)));
|
||||||
|
paths.add(found.toAbsolutePath().toString());
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName()));
|
||||||
|
Files.delete(found);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.yellow(" Library not found (even after scan): " + lib.name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,14 +545,34 @@ public class LaunchCommandBuilder {
|
|||||||
|
|
||||||
Path versionJar = findVersionJar();
|
Path versionJar = findVersionJar();
|
||||||
if (versionJar != null) {
|
if (versionJar != null) {
|
||||||
paths.add(0, versionJar.toAbsolutePath().toString());
|
if (isValidJar(versionJar)) {
|
||||||
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
|
paths.add(0, versionJar.toAbsolutePath().toString());
|
||||||
|
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
|
||||||
|
} else {
|
||||||
|
System.out.println(ZAnsi.yellow(" Corrupt version jar, deleting: " + versionJar.getFileName()));
|
||||||
|
Files.delete(versionJar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
||||||
return String.join(separator, paths);
|
return String.join(separator, paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path scanForJar(Path dir, String artifactName) throws IOException {
|
||||||
|
if (!Files.exists(dir)) return null;
|
||||||
|
try (var stream = Files.walk(dir)) {
|
||||||
|
return stream
|
||||||
|
.filter(p -> p.toString().endsWith(".jar"))
|
||||||
|
.filter(p -> {
|
||||||
|
String name = p.getFileName().toString();
|
||||||
|
// Match JARs like "artifactName-version.jar" but not "artifactName-other-version.jar"
|
||||||
|
return name.startsWith(artifactName + "-") || name.equals(artifactName + ".jar");
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String buildVanillaClasspath() throws Exception {
|
private String buildVanillaClasspath() throws Exception {
|
||||||
List<String> paths = new ArrayList<>();
|
List<String> paths = new ArrayList<>();
|
||||||
String versionId = getVersionId();
|
String versionId = getVersionId();
|
||||||
@@ -491,4 +714,69 @@ public class LaunchCommandBuilder {
|
|||||||
|
|
||||||
return jvmArgs;
|
return jvmArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidJar(Path path) {
|
||||||
|
if (path == null || !Files.exists(path)) return false;
|
||||||
|
try (ZipFile zf = new ZipFile(path.toFile())) {
|
||||||
|
return true;
|
||||||
|
} catch (ZipException e) {
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes classpath entries that are already listed on the module path
|
||||||
|
* (from -p or --module-path in manifest JVM arguments). Prevents split-package
|
||||||
|
* conflicts when --add-modules=ALL-MODULE-PATH resolves classpath JARs
|
||||||
|
* as automatic modules.
|
||||||
|
*/
|
||||||
|
private String filterClasspathAgainstModulePath(String classpath, List<String> manifestJvmArgs) {
|
||||||
|
if (classpath == null || classpath.isEmpty() || manifestJvmArgs == null || manifestJvmArgs.isEmpty()) {
|
||||||
|
return classpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> modulePathFilenames = new HashSet<>();
|
||||||
|
|
||||||
|
// Parse -p / --module-path values from manifest JVM arguments
|
||||||
|
for (int i = 0; i < manifestJvmArgs.size(); i++) {
|
||||||
|
String arg = manifestJvmArgs.get(i);
|
||||||
|
if (("-p".equals(arg) || "--module-path".equals(arg)) && i + 1 < manifestJvmArgs.size()) {
|
||||||
|
String modulePathValue = manifestJvmArgs.get(i + 1);
|
||||||
|
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
||||||
|
for (String entry : modulePathValue.split(sep)) {
|
||||||
|
int lastSep = Math.max(entry.lastIndexOf('/'), entry.lastIndexOf('\\'));
|
||||||
|
modulePathFilenames.add(lastSep >= 0 ? entry.substring(lastSep + 1) : entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modulePathFilenames.isEmpty()) return classpath;
|
||||||
|
|
||||||
|
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
||||||
|
StringBuilder filtered = new StringBuilder();
|
||||||
|
for (String entry : classpath.split(separator)) {
|
||||||
|
if (entry.isEmpty()) continue;
|
||||||
|
int lastSep = Math.max(entry.lastIndexOf('/'), entry.lastIndexOf('\\'));
|
||||||
|
String filename = lastSep >= 0 ? entry.substring(lastSep + 1) : entry;
|
||||||
|
if (!modulePathFilenames.contains(filename)) {
|
||||||
|
if (filtered.length() > 0) filtered.append(separator);
|
||||||
|
filtered.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = filtered.toString();
|
||||||
|
return result.isEmpty() ? classpath : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For very long classpaths (Windows cmd truncation), write to a temp argfile
|
||||||
|
private String writeClasspathFile(String classpath) throws IOException {
|
||||||
|
if (classpath.length() < 8000) return classpath;
|
||||||
|
Path tempFile = Files.createTempFile("zernmc-cp-", ".txt");
|
||||||
|
Files.writeString(tempFile, classpath);
|
||||||
|
tempFiles.add(tempFile);
|
||||||
|
System.out.println(ZAnsi.cyan(" Classpath too long (" + classpath.length() + " chars), using argfile: " + tempFile));
|
||||||
|
return "@" + tempFile.toAbsolutePath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-20
@@ -15,13 +15,15 @@ public class VersionManifest {
|
|||||||
private final String id;
|
private final String id;
|
||||||
private final String mainClass;
|
private final String mainClass;
|
||||||
private final String assetIndexId;
|
private final String assetIndexId;
|
||||||
|
private final String inheritsFrom;
|
||||||
private final List<String> jvmArguments;
|
private final List<String> jvmArguments;
|
||||||
private final List<String> gameArguments;
|
private final List<String> gameArguments;
|
||||||
private final List<Library> libraries;
|
private final List<Library> libraries;
|
||||||
|
private VersionManifest parent;
|
||||||
|
|
||||||
public VersionManifest(JSONObject json) {
|
public VersionManifest(JSONObject json) {
|
||||||
this.id = json.getString("id");
|
this.id = json.getString("id");
|
||||||
this.mainClass = json.getString("mainClass");
|
this.mainClass = json.optString("mainClass", "net.minecraft.client.main.Main");
|
||||||
|
|
||||||
if (json.has("assetIndex")) {
|
if (json.has("assetIndex")) {
|
||||||
JSONObject ai = json.getJSONObject("assetIndex");
|
JSONObject ai = json.getJSONObject("assetIndex");
|
||||||
@@ -30,6 +32,7 @@ public class VersionManifest {
|
|||||||
this.assetIndexId = "unknown";
|
this.assetIndexId = "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.inheritsFrom = json.optString("inheritsFrom", null);
|
||||||
this.jvmArguments = parseArguments(json, "jvm");
|
this.jvmArguments = parseArguments(json, "jvm");
|
||||||
this.gameArguments = parseArguments(json, "game");
|
this.gameArguments = parseArguments(json, "game");
|
||||||
this.libraries = parseLibraries(json);
|
this.libraries = parseLibraries(json);
|
||||||
@@ -38,10 +41,47 @@ public class VersionManifest {
|
|||||||
public String getId() { return id; }
|
public String getId() { return id; }
|
||||||
public String getMainClass() { return mainClass; }
|
public String getMainClass() { return mainClass; }
|
||||||
public String getAssetIndexId() { return assetIndexId; }
|
public String getAssetIndexId() { return assetIndexId; }
|
||||||
|
public String getInheritsFrom() { return inheritsFrom; }
|
||||||
public List<String> getJvmArguments() { return jvmArguments; }
|
public List<String> getJvmArguments() { return jvmArguments; }
|
||||||
public List<String> getGameArguments() { return gameArguments; }
|
public List<String> getGameArguments() { return gameArguments; }
|
||||||
public List<Library> getLibraries() { return libraries; }
|
public List<Library> getLibraries() { return libraries; }
|
||||||
|
|
||||||
|
public void resolveParent(Path versionsDir) {
|
||||||
|
if (inheritsFrom == null || parent != null) return;
|
||||||
|
Path parentDir = versionsDir.resolve(inheritsFrom);
|
||||||
|
Path parentJson = parentDir.resolve(inheritsFrom + ".json");
|
||||||
|
if (Files.exists(parentJson)) {
|
||||||
|
try {
|
||||||
|
String content = Files.readString(parentJson);
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
this.parent = new VersionManifest(json);
|
||||||
|
this.parent.resolveParent(versionsDir);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("Failed to load parent version " + inheritsFrom + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Library> getAllLibraries() {
|
||||||
|
List<Library> all = new ArrayList<>();
|
||||||
|
if (parent != null) {
|
||||||
|
all.addAll(parent.getAllLibraries());
|
||||||
|
}
|
||||||
|
for (Library lib : libraries) {
|
||||||
|
if (!containsLibrary(all, lib.name)) {
|
||||||
|
all.add(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsLibrary(List<Library> list, String name) {
|
||||||
|
for (Library l : list) {
|
||||||
|
if (l.name.equals(name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> parseArguments(JSONObject json, String type) {
|
private List<String> parseArguments(JSONObject json, String type) {
|
||||||
List<String> args = new ArrayList<>();
|
List<String> args = new ArrayList<>();
|
||||||
if (!json.has("arguments")) return args;
|
if (!json.has("arguments")) return args;
|
||||||
@@ -129,29 +169,30 @@ public class VersionManifest {
|
|||||||
JSONArray arr = json.getJSONArray("libraries");
|
JSONArray arr = json.getJSONArray("libraries");
|
||||||
for (int i = 0; i < arr.length(); i++) {
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
JSONObject libJson = arr.getJSONObject(i);
|
JSONObject libJson = arr.getJSONObject(i);
|
||||||
|
String name = libJson.getString("name");
|
||||||
|
String artifactPath = null;
|
||||||
if (libJson.has("downloads") && libJson.getJSONObject("downloads").has("artifact")) {
|
if (libJson.has("downloads") && libJson.getJSONObject("downloads").has("artifact")) {
|
||||||
String name = libJson.getString("name");
|
artifactPath = libJson.getJSONObject("downloads").getJSONObject("artifact").getString("path");
|
||||||
String artifactPath = libJson.getJSONObject("downloads").getJSONObject("artifact").getString("path");
|
}
|
||||||
Library lib = new Library(name, artifactPath);
|
Library lib = new Library(name, artifactPath);
|
||||||
|
|
||||||
if (libJson.has("natives")) {
|
if (libJson.has("natives")) {
|
||||||
JSONObject natives = libJson.getJSONObject("natives");
|
JSONObject natives = libJson.getJSONObject("natives");
|
||||||
for (String key : natives.keySet()) {
|
for (String key : natives.keySet()) {
|
||||||
String osKey = key.toLowerCase();
|
String osKey = key.toLowerCase();
|
||||||
lib.natives.put(osKey, natives.getString(key));
|
lib.natives.put(osKey, natives.getString(key));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (libJson.has("rules")) {
|
boolean allowed = true;
|
||||||
JSONObject dummyObj = new JSONObject();
|
if (libJson.has("rules")) {
|
||||||
dummyObj.put("rules", libJson.getJSONArray("rules"));
|
JSONObject dummyObj = new JSONObject();
|
||||||
dummyObj.put("value", "");
|
dummyObj.put("rules", libJson.getJSONArray("rules"));
|
||||||
if (ruleMatches(dummyObj)) {
|
dummyObj.put("value", "");
|
||||||
libs.add(lib);
|
allowed = ruleMatches(dummyObj);
|
||||||
}
|
}
|
||||||
} else {
|
if (allowed) {
|
||||||
libs.add(lib);
|
libs.add(lib);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return libs;
|
return libs;
|
||||||
|
|||||||
@@ -713,8 +713,9 @@ public class JFXLauncher extends Application {
|
|||||||
String version = body.get("version");
|
String version = body.get("version");
|
||||||
String loader = body.get("loader");
|
String loader = body.get("loader");
|
||||||
String loaderVersion = body.get("loaderVersion");
|
String loaderVersion = body.get("loaderVersion");
|
||||||
|
String preset = body.get("preset");
|
||||||
|
|
||||||
log("Install: " + name + " " + version + " " + loader + (loaderVersion != null ? " " + loaderVersion : ""));
|
log("Install: " + name + " " + version + " " + loader + (loaderVersion != null ? " " + loaderVersion : "") + (preset != null ? " preset=" + preset : ""));
|
||||||
|
|
||||||
var createResult = api.instances().createInstance(name);
|
var createResult = api.instances().createInstance(name);
|
||||||
if (!createResult.isSuccess()) {
|
if (!createResult.isSuccess()) {
|
||||||
@@ -742,24 +743,35 @@ public class JFXLauncher extends Application {
|
|||||||
boolean success = false;
|
boolean success = false;
|
||||||
|
|
||||||
if ("zernmc".equalsIgnoreCase(loader)) {
|
if ("zernmc".equalsIgnoreCase(loader)) {
|
||||||
|
log("[DEBUG] Starting zernmc pack install for version=" + version);
|
||||||
setInstallProgressWithStage("Fetching pack info...", 10, 100, "Fetching pack info", 0, 5);
|
setInstallProgressWithStage("Fetching pack info...", 10, 100, "Fetching pack info", 0, 5);
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
downloader.setProgressCallback((label, pct, stageName, stageIdx, stageCount) -> {
|
downloader.setProgressCallback((label, pct, stageName, stageIdx, stageCount) -> {
|
||||||
setInstallProgressWithStage(label, pct, 100, stageName, stageIdx, stageCount);
|
setInstallProgressWithStage(label, pct, 100, stageName, stageIdx, stageCount);
|
||||||
});
|
});
|
||||||
|
log("[DEBUG] Getting available packs...");
|
||||||
var packs = downloader.getAvailablePacks();
|
var packs = downloader.getAvailablePacks();
|
||||||
|
log("[DEBUG] Available packs: " + (packs != null ? packs.size() : "null"));
|
||||||
ServerPack pack = null;
|
ServerPack pack = null;
|
||||||
for (ServerPack p : packs) {
|
for (ServerPack p : packs) {
|
||||||
|
log("[DEBUG] Checking pack: " + p.getName() + " vs " + version);
|
||||||
if (p.getName().equals(version)) {
|
if (p.getName().equals(version)) {
|
||||||
pack = p;
|
pack = p;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log("[DEBUG] Selected pack: " + (pack != null ? pack.getName() : "null"));
|
||||||
if (pack != null) {
|
if (pack != null) {
|
||||||
try {
|
try {
|
||||||
success = downloader.installOrUpdatePack(version, pack);
|
log("[DEBUG] Calling installOrUpdatePack..." + (preset != null ? " preset=" + preset : ""));
|
||||||
|
if (preset != null && !preset.isEmpty()) {
|
||||||
|
instance.setPresetId(preset);
|
||||||
|
}
|
||||||
|
success = downloader.installOrUpdatePack(version, pack, preset);
|
||||||
|
log("[DEBUG] installOrUpdatePack returned: " + success);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log("Pack install error: " + e.getMessage());
|
log("Pack install error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -796,7 +808,6 @@ public class JFXLauncher extends Application {
|
|||||||
setInstallProgressWithStage("Error: " + e.getMessage(), 0, 100, "Error", 0, 1);
|
setInstallProgressWithStage("Error: " + e.getMessage(), 0, 100, "Error", 0, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
installThread.setDaemon(true);
|
|
||||||
installThread.start();
|
installThread.start();
|
||||||
} else {
|
} else {
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Instance not found"));
|
sendJson(exchange, Map.of("success", false, "error", "Instance not found"));
|
||||||
@@ -1144,6 +1155,7 @@ public class JFXLauncher extends Application {
|
|||||||
data.put("systemBasedJvm", Config.isSystemBasedJvm());
|
data.put("systemBasedJvm", Config.isSystemBasedJvm());
|
||||||
data.put("cpuCores", Config.getSystemCpuCores());
|
data.put("cpuCores", Config.getSystemCpuCores());
|
||||||
data.put("totalRamMB", Config.getSystemTotalRamMB());
|
data.put("totalRamMB", Config.getSystemTotalRamMB());
|
||||||
|
data.put("systemJvmFlags", Config.getSystemJvmFlags());
|
||||||
sendJson(exchange, Map.of("success", true, "data", data));
|
sendJson(exchange, Map.of("success", true, "data", data));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
||||||
@@ -1156,7 +1168,10 @@ public class JFXLauncher extends Application {
|
|||||||
for (String pair : query.split("&")) {
|
for (String pair : query.split("&")) {
|
||||||
String[] kv = pair.split("=");
|
String[] kv = pair.split("=");
|
||||||
if (kv.length == 2) {
|
if (kv.length == 2) {
|
||||||
params.put(kv[0], kv[1]);
|
try {
|
||||||
|
params.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8),
|
||||||
|
URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1408,12 +1423,11 @@ public class JFXLauncher extends Application {
|
|||||||
private void handleCheckUpdates(HttpExchange exchange) {
|
private void handleCheckUpdates(HttpExchange exchange) {
|
||||||
try {
|
try {
|
||||||
Map<String, String> params = parseQuery(exchange.getRequestURI().getQuery());
|
Map<String, String> params = parseQuery(exchange.getRequestURI().getQuery());
|
||||||
String rawName = params.get("name");
|
String name = params.get("name");
|
||||||
if (rawName == null || rawName.isBlank()) {
|
if (name == null || name.isBlank()) {
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Missing 'name' parameter"));
|
sendJson(exchange, Map.of("success", false, "error", "Missing 'name' parameter"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String name = URLDecoder.decode(rawName, StandardCharsets.UTF_8);
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
Instance instance = InstanceManager.getInstance(name);
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Instance not found"));
|
sendJson(exchange, Map.of("success", false, "error", "Instance not found"));
|
||||||
@@ -1425,7 +1439,12 @@ public class JFXLauncher extends Application {
|
|||||||
}
|
}
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
String packName = instance.getServerPackName();
|
String packName = instance.getServerPackName();
|
||||||
int serverVersion = downloader.getPackManifest(packName).getVersion();
|
var manifest = downloader.getPackManifest(packName);
|
||||||
|
if (manifest == null) {
|
||||||
|
sendJson(exchange, Map.of("success", false, "error", "Failed to fetch pack manifest"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int serverVersion = manifest.getVersion();
|
||||||
int localVersion = instance.getServerVersion();
|
int localVersion = instance.getServerVersion();
|
||||||
sendJson(exchange, Map.of(
|
sendJson(exchange, Map.of(
|
||||||
"success", true,
|
"success", true,
|
||||||
@@ -1458,7 +1477,14 @@ public class JFXLauncher extends Application {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
boolean success = downloader.updatePack(instance.getServerPackName());
|
String packName = instance.getServerPackName();
|
||||||
|
String preset = instance.getPresetId();
|
||||||
|
boolean success;
|
||||||
|
if (preset != null && !preset.isEmpty()) {
|
||||||
|
success = downloader.updatePack(packName, preset);
|
||||||
|
} else {
|
||||||
|
success = downloader.updatePack(packName);
|
||||||
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
sendJson(exchange, Map.of("success", true, "data", Map.of("version", instance.getServerVersion())));
|
sendJson(exchange, Map.of("success", true, "data", Map.of("version", instance.getServerVersion())));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ public class Config {
|
|||||||
private static final Path CONFIG_DIR = Path.of(System.getProperty("user.home"), ".zernmc");
|
private static final Path CONFIG_DIR = Path.of(System.getProperty("user.home"), ".zernmc");
|
||||||
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("launcher.properties");
|
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("launcher.properties");
|
||||||
|
|
||||||
private static final String BUILD_PROFILE = System.getProperty("build.profile", "global");
|
|
||||||
|
|
||||||
private static final Properties props = new Properties();
|
private static final Properties props = new Properties();
|
||||||
|
|
||||||
private static volatile int maxMemory = 4096;
|
private static volatile int maxMemory = 4096;
|
||||||
@@ -133,14 +131,6 @@ public class Config {
|
|||||||
return maxMemory;
|
return maxMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isZernMCBuild() {
|
|
||||||
return "zernmc".equalsIgnoreCase(BUILD_PROFILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isGlobalBuild() {
|
|
||||||
return !isZernMCBuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setMaxMemory(int memory) {
|
public static void setMaxMemory(int memory) {
|
||||||
if (memory < 1024) memory = 1536;
|
if (memory < 1024) memory = 1536;
|
||||||
if (memory > 32768) memory = 32768;
|
if (memory > 32768) memory = 32768;
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ public class LauncherLogger {
|
|||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path logsDir = Paths.get(System.getProperty("user.home"), ".zernmc", "logs");
|
Path logsDir = Paths.get("").toAbsolutePath().resolve("logs");
|
||||||
Files.createDirectories(logsDir);
|
Files.createDirectories(logsDir);
|
||||||
logFile = logsDir.resolve("launcher.log");
|
logFile = logsDir.resolve("launcher.log");
|
||||||
|
|
||||||
Files.writeString(logFile,
|
Files.writeString(logFile,
|
||||||
"=== Launcher Log " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ===\n",
|
"=== Launcher Log " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ===\n",
|
||||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||||
|
|
||||||
System.out.println("[LauncherLogger] initialized, log: " + logFile.toAbsolutePath());
|
System.out.println("[LauncherLogger] initialized, log: " + logFile.toAbsolutePath());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -332,12 +332,20 @@ public class ZHttpClient {
|
|||||||
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
|
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
|
||||||
if (!shouldUseProxyForUrl(url)) {
|
if (!shouldUseProxyForUrl(url)) {
|
||||||
try {
|
try {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(url))
|
||||||
.timeout(Duration.ofSeconds(40))
|
.timeout(Duration.ofSeconds(40))
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
.header("User-Agent", "ZernMC-Launcher/1.0")
|
||||||
.GET()
|
.GET();
|
||||||
.build();
|
|
||||||
|
if (url.startsWith(BASE_URL)) {
|
||||||
|
String accessToken = AuthManager.getAccessToken();
|
||||||
|
if (accessToken != null && !accessToken.equals("0")) {
|
||||||
|
requestBuilder.header("Authorization", "Bearer " + accessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest request = requestBuilder.build();
|
||||||
|
|
||||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,9 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-title" data-i18n="sidebar.serverPacks">Server Packs</span>
|
<span class="section-title" data-i18n="sidebar.serverPacks">Server Packs</span>
|
||||||
|
<button class="btn-icon" id="add-server-pack-btn" title="Install server pack">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="server-packs-list" class="pack-list"></div>
|
<div id="server-packs-list" class="pack-list"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,6 +201,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="inline-install-progress" class="inline-install-progress hidden">
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-fill" id="inline-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-label" id="inline-progress-label">Installing...</p>
|
||||||
|
<p class="progress-stage hidden" id="inline-progress-stage"></p>
|
||||||
|
</div>
|
||||||
<div class="play-bar" id="play-bar">
|
<div class="play-bar" id="play-bar">
|
||||||
<div class="play-bar-info">
|
<div class="play-bar-info">
|
||||||
<span id="play-bar-name">Select a pack</span>
|
<span id="play-bar-name">Select a pack</span>
|
||||||
@@ -253,6 +263,7 @@
|
|||||||
<button class="admin-tab" data-atab="mods"><span data-i18n="admin.mods">Mods</span></button>
|
<button class="admin-tab" data-atab="mods"><span data-i18n="admin.mods">Mods</span></button>
|
||||||
<button class="admin-tab" data-atab="users"><span data-i18n="admin.users">Users</span></button>
|
<button class="admin-tab" data-atab="users"><span data-i18n="admin.users">Users</span></button>
|
||||||
<button class="admin-tab" data-atab="passes"><span data-i18n="admin.passes">Passes</span></button>
|
<button class="admin-tab" data-atab="passes"><span data-i18n="admin.passes">Passes</span></button>
|
||||||
|
<button class="admin-tab" data-atab="packs"><span data-i18n="admin.packs">Packs</span></button>
|
||||||
<button class="admin-tab" data-atab="news"><span data-i18n="admin.news">News</span></button>
|
<button class="admin-tab" data-atab="news"><span data-i18n="admin.news">News</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-content">
|
<div class="admin-content">
|
||||||
@@ -315,6 +326,13 @@
|
|||||||
<div id="admin-passes-list" class="admin-list"><div class="admin-loading">Loading...</div></div>
|
<div id="admin-passes-list" class="admin-list"><div class="admin-loading">Loading...</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Packs -->
|
||||||
|
<div id="atab-packs" class="admin-tab-content">
|
||||||
|
<div class="admin-card">
|
||||||
|
<div class="admin-card-header"><span data-i18n="admin.packsList">Pack Management</span></div>
|
||||||
|
<div id="admin-packs-list" class="admin-list"><div class="admin-loading">Loading...</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<div id="atab-news" class="admin-tab-content">
|
<div id="atab-news" class="admin-tab-content">
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
@@ -506,6 +524,12 @@
|
|||||||
<option value="">Loading...</option>
|
<option value="">Loading...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="preset-select-container" class="field hidden">
|
||||||
|
<label data-i18n="install.preset.label">Preset (optional)</label>
|
||||||
|
<select id="preset-select">
|
||||||
|
<option value=""><span data-i18n="install.preset.default">Default</span></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button id="install-zernmc-btn" class="btn-primary"><span data-i18n="install.downloadBtn">Download & Install</span></button>
|
<button id="install-zernmc-btn" class="btn-primary"><span data-i18n="install.downloadBtn">Download & Install</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -163,9 +163,19 @@ const LOCALES = {
|
|||||||
'admin.newsCreated': 'News published!',
|
'admin.newsCreated': 'News published!',
|
||||||
'admin.newsDeleted': 'News deleted',
|
'admin.newsDeleted': 'News deleted',
|
||||||
'admin.newsEmpty': 'No news',
|
'admin.newsEmpty': 'No news',
|
||||||
|
'admin.packs': 'Packs',
|
||||||
|
'admin.packsList': 'Pack Management',
|
||||||
|
'admin.packsEmpty': 'No packs',
|
||||||
|
'admin.packsToggle': 'Toggle',
|
||||||
|
'admin.packsDisabled': 'Disabled',
|
||||||
|
'admin.packsEnabled': 'Enabled',
|
||||||
|
'admin.packsStatus': 'Status',
|
||||||
'admin.delete': 'Delete',
|
'admin.delete': 'Delete',
|
||||||
'admin.loading': 'Loading...',
|
'admin.loading': 'Loading...',
|
||||||
'admin.error': 'Error',
|
'admin.error': 'Error',
|
||||||
|
'install.preset.label': 'Preset (optional)',
|
||||||
|
'install.preset.default': 'Default',
|
||||||
|
'pack.disabled': 'Disabled',
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
'nav.packs': 'Сборки', 'nav.news': 'Новости', 'nav.settings': 'Настройки',
|
'nav.packs': 'Сборки', 'nav.news': 'Новости', 'nav.settings': 'Настройки',
|
||||||
@@ -329,9 +339,19 @@ const LOCALES = {
|
|||||||
'admin.newsCreated': 'Новость опубликована!',
|
'admin.newsCreated': 'Новость опубликована!',
|
||||||
'admin.newsDeleted': 'Новость удалена',
|
'admin.newsDeleted': 'Новость удалена',
|
||||||
'admin.newsEmpty': 'Нет новостей',
|
'admin.newsEmpty': 'Нет новостей',
|
||||||
|
'admin.packs': 'Сборки',
|
||||||
|
'admin.packsList': 'Управление сборками',
|
||||||
|
'admin.packsEmpty': 'Нет сборок',
|
||||||
|
'admin.packsToggle': 'Переключить',
|
||||||
|
'admin.packsDisabled': 'Отключена',
|
||||||
|
'admin.packsEnabled': 'Включена',
|
||||||
|
'admin.packsStatus': 'Статус',
|
||||||
'admin.delete': 'Удалить',
|
'admin.delete': 'Удалить',
|
||||||
'admin.loading': 'Загрузка...',
|
'admin.loading': 'Загрузка...',
|
||||||
'admin.error': 'Ошибка',
|
'admin.error': 'Ошибка',
|
||||||
|
'install.preset.label': 'Пресет (опционально)',
|
||||||
|
'install.preset.default': 'Стандартный',
|
||||||
|
'pack.disabled': 'Отключена',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -570,6 +590,10 @@ class ZernMCLauncher {
|
|||||||
const r = await this.req('/packs');
|
const r = await this.req('/packs');
|
||||||
if (r.success && r.data) {
|
if (r.success && r.data) {
|
||||||
this.state.serverPacks = r.data;
|
this.state.serverPacks = r.data;
|
||||||
|
this.state.serverPacksMap = {};
|
||||||
|
r.data.forEach(function(p) {
|
||||||
|
app.state.serverPacksMap[p.name] = p;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +608,7 @@ class ZernMCLauncher {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('add-pack-btn').addEventListener('click', () => this.showInstallModal());
|
document.getElementById('add-pack-btn').addEventListener('click', () => this.showInstallModal());
|
||||||
|
document.getElementById('add-server-pack-btn').addEventListener('click', () => this.showInstallModal());
|
||||||
document.getElementById('close-modal-btn').addEventListener('click', () => this.hideInstallModal());
|
document.getElementById('close-modal-btn').addEventListener('click', () => this.hideInstallModal());
|
||||||
document.querySelectorAll('.modal-tab').forEach(t => {
|
document.querySelectorAll('.modal-tab').forEach(t => {
|
||||||
t.addEventListener('click', () => this.switchInstallTab(t.dataset.tab));
|
t.addEventListener('click', () => this.switchInstallTab(t.dataset.tab));
|
||||||
@@ -673,6 +698,10 @@ class ZernMCLauncher {
|
|||||||
if (!this.state.instances) return;
|
if (!this.state.instances) return;
|
||||||
for (const inst of this.state.instances) {
|
for (const inst of this.state.instances) {
|
||||||
if (!inst.isServerPack) continue;
|
if (!inst.isServerPack) continue;
|
||||||
|
// Skip disabled packs
|
||||||
|
if (this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
|
const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
|
||||||
if (r.success && r.data) {
|
if (r.success && r.data) {
|
||||||
this.state.updateMap[inst.name] = r.data.hasUpdate;
|
this.state.updateMap[inst.name] = r.data.hasUpdate;
|
||||||
@@ -682,6 +711,10 @@ class ZernMCLauncher {
|
|||||||
|
|
||||||
async checkUpdateForPack(inst) {
|
async checkUpdateForPack(inst) {
|
||||||
if (!inst || !inst.isServerPack) return;
|
if (!inst || !inst.isServerPack) return;
|
||||||
|
// Skip disabled packs
|
||||||
|
if (this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
|
const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
|
||||||
if (r.success && r.data) {
|
if (r.success && r.data) {
|
||||||
if (!this.state.updateMap) this.state.updateMap = {};
|
if (!this.state.updateMap) this.state.updateMap = {};
|
||||||
@@ -706,9 +739,11 @@ class ZernMCLauncher {
|
|||||||
this.state.instances.forEach(inst => {
|
this.state.instances.forEach(inst => {
|
||||||
const isZern = inst.isServerPack || inst.category === 'zernmc';
|
const isZern = inst.isServerPack || inst.category === 'zernmc';
|
||||||
const targetList = isZern ? serverList : localList;
|
const targetList = isZern ? serverList : localList;
|
||||||
|
const isDisabled = this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled;
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'pack-entry' + (this.state.selectedPack && this.state.selectedPack.name === inst.name ? ' selected' : '');
|
el.className = 'pack-entry' + (this.state.selectedPack && this.state.selectedPack.name === inst.name ? ' selected' : '') + (isDisabled ? ' disabled' : '');
|
||||||
|
if (isDisabled) el.setAttribute('data-disabled-text', t('pack.disabled'));
|
||||||
const hasUpd = this.hasUpdate(inst);
|
const hasUpd = this.hasUpdate(inst);
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="pack-entry-icon ${isZern ? 'server' : 'local'}">
|
<div class="pack-entry-icon ${isZern ? 'server' : 'local'}">
|
||||||
@@ -716,14 +751,16 @@ class ZernMCLauncher {
|
|||||||
? '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>'
|
? '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>'
|
||||||
: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'
|
: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'
|
||||||
}</svg>
|
}</svg>
|
||||||
${hasUpd ? '<span class="update-badge"></span>' : ''}
|
${hasUpd && !isDisabled ? '<span class="update-badge"></span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="pack-entry-info">
|
<div class="pack-entry-info">
|
||||||
<div class="pack-entry-name">${this.esc(inst.name)}${hasUpd ? ' <span class="update-label">' + t('pack.update') + '</span>' : ''}</div>
|
<div class="pack-entry-name">${this.esc(inst.name)}${hasUpd && !isDisabled ? ' <span class="update-label">' + t('pack.update') + '</span>' : ''}</div>
|
||||||
<div class="pack-entry-meta">${inst.version || '?'}${inst.loaderType && inst.loaderType !== 'vanilla' ? ' \u00b7 ' + inst.loaderType : ''}</div>
|
<div class="pack-entry-meta">${inst.version || '?'}${inst.loaderType && inst.loaderType !== 'vanilla' ? ' \u00b7 ' + inst.loaderType : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
el.addEventListener('click', () => this.selectPack(inst));
|
if (!isDisabled) {
|
||||||
|
el.addEventListener('click', () => this.selectPack(inst));
|
||||||
|
}
|
||||||
targetList.appendChild(el);
|
targetList.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -965,6 +1002,7 @@ class ZernMCLauncher {
|
|||||||
if (tab === 'users') this.adminClearUser();
|
if (tab === 'users') this.adminClearUser();
|
||||||
if (tab === 'passes') this.adminLoadPasses();
|
if (tab === 'passes') this.adminLoadPasses();
|
||||||
if (tab === 'news') this.adminLoadNewsList();
|
if (tab === 'news') this.adminLoadNewsList();
|
||||||
|
if (tab === 'packs') this.adminLoadPacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Clients ---
|
// --- Clients ---
|
||||||
@@ -1229,6 +1267,48 @@ class ZernMCLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Packs ---
|
||||||
|
async adminLoadPacks() {
|
||||||
|
var el = document.getElementById('admin-packs-list');
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
|
||||||
|
var r = await this.req('/admin/packs');
|
||||||
|
if (!r.success) {
|
||||||
|
el.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var packs = r.packs || [];
|
||||||
|
if (!packs.length) {
|
||||||
|
el.innerHTML = '<div class="admin-empty">' + t('admin.packsEmpty') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '';
|
||||||
|
packs.forEach(function(p) {
|
||||||
|
var statusText = p.disabled ? t('admin.packsDisabled') : t('admin.packsEnabled');
|
||||||
|
var statusClass = p.disabled ? 'admin-pack-disabled' : 'admin-pack-enabled';
|
||||||
|
html += '<div class="admin-item">'
|
||||||
|
+ '<span class="admin-item-name">' + app.esc(p.name) + '</span>'
|
||||||
|
+ '<span class="admin-item-meta ' + statusClass + '">' + statusText + '</span>'
|
||||||
|
+ '<div class="admin-item-actions">'
|
||||||
|
+ '<button class="btn-secondary btn-sm" onclick="app.adminTogglePack(\'' + app.esc(p.name) + '\')">' + t('admin.packsToggle') + '</button>'
|
||||||
|
+ '</div></div>';
|
||||||
|
});
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminTogglePack(packName) {
|
||||||
|
var r = await this.req('/admin/packs/' + encodeURIComponent(packName) + '/toggle', {
|
||||||
|
method: 'POST',
|
||||||
|
body: '{}'
|
||||||
|
});
|
||||||
|
if (r.success) {
|
||||||
|
this.toast((r.disabled ? t('admin.packsDisabled') : t('admin.packsEnabled')) + ': ' + packName, 'success');
|
||||||
|
this.adminLoadPacks();
|
||||||
|
} else {
|
||||||
|
this.toast(r.error || t('admin.error'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== NEWS ====================
|
// ==================== NEWS ====================
|
||||||
async loadNews() {
|
async loadNews() {
|
||||||
const r = await this.req('/news');
|
const r = await this.req('/news');
|
||||||
@@ -1369,10 +1449,13 @@ class ZernMCLauncher {
|
|||||||
async showInstallModal() {
|
async showInstallModal() {
|
||||||
document.getElementById('install-modal').classList.remove('hidden');
|
document.getElementById('install-modal').classList.remove('hidden');
|
||||||
const zernmcSel = document.getElementById('zernmc-pack-select');
|
const zernmcSel = document.getElementById('zernmc-pack-select');
|
||||||
|
const presetContainer = document.getElementById('preset-select-container');
|
||||||
|
const presetSel = document.getElementById('preset-select');
|
||||||
const mcSel = document.getElementById('mc-version-select');
|
const mcSel = document.getElementById('mc-version-select');
|
||||||
if (zernmcSel) {
|
if (zernmcSel) {
|
||||||
zernmcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
|
zernmcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
|
||||||
}
|
}
|
||||||
|
if (presetContainer) presetContainer.classList.add('hidden');
|
||||||
if (mcSel) {
|
if (mcSel) {
|
||||||
mcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
|
mcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
|
||||||
}
|
}
|
||||||
@@ -1384,11 +1467,35 @@ class ZernMCLauncher {
|
|||||||
zernmcSel.innerHTML = '<option value="">' + t('select.selectPack') + '</option>';
|
zernmcSel.innerHTML = '<option value="">' + t('select.selectPack') + '</option>';
|
||||||
zernmcSel.disabled = false;
|
zernmcSel.disabled = false;
|
||||||
packs.data.forEach(p => {
|
packs.data.forEach(p => {
|
||||||
|
if (p.disabled) return; // skip disabled packs
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = p.name;
|
o.value = p.name;
|
||||||
|
o.dataset.presets = JSON.stringify(p.presets || []);
|
||||||
o.textContent = (p.displayName || p.name) + ' (' + (p.version || '') + ')';
|
o.textContent = (p.displayName || p.name) + ' (' + (p.version || '') + ')';
|
||||||
zernmcSel.appendChild(o);
|
zernmcSel.appendChild(o);
|
||||||
});
|
});
|
||||||
|
// Update preset selector when pack changes
|
||||||
|
zernmcSel.onchange = function() {
|
||||||
|
const selected = zernmcSel.options[zernmcSel.selectedIndex];
|
||||||
|
const presets = selected && selected.dataset.presets ? JSON.parse(selected.dataset.presets) : [];
|
||||||
|
if (presetSel) {
|
||||||
|
presetSel.innerHTML = '<option value="">' + t('install.preset.default') + '</option>';
|
||||||
|
presets.forEach(function(pr) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = pr;
|
||||||
|
o.textContent = pr;
|
||||||
|
presetSel.appendChild(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (presetContainer) {
|
||||||
|
if (presets.length > 0) {
|
||||||
|
presetContainer.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
presetContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.enhanceSelects();
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
zernmcSel.innerHTML = '<option value="">' + t('select.passRequired') + '</option>';
|
zernmcSel.innerHTML = '<option value="">' + t('select.passRequired') + '</option>';
|
||||||
zernmcSel.disabled = true;
|
zernmcSel.disabled = true;
|
||||||
@@ -1436,7 +1543,9 @@ class ZernMCLauncher {
|
|||||||
hideInstallModal() {
|
hideInstallModal() {
|
||||||
document.getElementById('install-modal').classList.add('hidden');
|
document.getElementById('install-modal').classList.add('hidden');
|
||||||
document.getElementById('install-progress').classList.add('hidden');
|
document.getElementById('install-progress').classList.add('hidden');
|
||||||
this.stopProgressPoll();
|
if (this.progressPoller) {
|
||||||
|
document.getElementById('inline-install-progress').classList.remove('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchInstallTab(tab) {
|
switchInstallTab(tab) {
|
||||||
@@ -1584,12 +1693,16 @@ class ZernMCLauncher {
|
|||||||
async installZernMCPack() {
|
async installZernMCPack() {
|
||||||
const packName = document.getElementById('zernmc-pack-select').value;
|
const packName = document.getElementById('zernmc-pack-select').value;
|
||||||
if (!packName) { this.toast(t('select.selectPack'), 'error'); return; }
|
if (!packName) { this.toast(t('select.selectPack'), 'error'); return; }
|
||||||
|
const preset = document.getElementById('preset-select') ? document.getElementById('preset-select').value : '';
|
||||||
// Use server pack name as instance name (no local naming)
|
// Use server pack name as instance name (no local naming)
|
||||||
const instanceName = packName;
|
const instanceName = packName;
|
||||||
|
|
||||||
|
const body = { name: instanceName, version: packName, loader: 'zernmc' };
|
||||||
|
if (preset) body.preset = preset;
|
||||||
|
|
||||||
const r = await this.req('/install', {
|
const r = await this.req('/install', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name: instanceName, version: packName, loader: 'zernmc' })
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
if (r.success) {
|
if (r.success) {
|
||||||
this.toast(t('toast.installing'), 'info');
|
this.toast(t('toast.installing'), 'info');
|
||||||
@@ -1624,23 +1737,43 @@ class ZernMCLauncher {
|
|||||||
this.progressPoller = setInterval(async () => {
|
this.progressPoller = setInterval(async () => {
|
||||||
const r = await this.req('/install/progress');
|
const r = await this.req('/install/progress');
|
||||||
if (r.success && r.data) {
|
if (r.success && r.data) {
|
||||||
const p = document.getElementById('install-progress');
|
const modalP = document.getElementById('install-progress');
|
||||||
p.classList.remove('hidden');
|
modalP.classList.remove('hidden');
|
||||||
document.getElementById('progress-fill').style.width = (r.data.percent || 0) + '%';
|
const pct = (r.data.percent || 0) + '%';
|
||||||
document.getElementById('progress-label').textContent = r.data.label || 'Installing...';
|
const label = r.data.label || 'Installing...';
|
||||||
const stageInfo = document.getElementById('progress-stage');
|
|
||||||
|
document.getElementById('progress-fill').style.width = pct;
|
||||||
|
document.getElementById('progress-label').textContent = label;
|
||||||
|
|
||||||
|
const inlineProgress = document.getElementById('inline-install-progress');
|
||||||
|
if (!inlineProgress.classList.contains('hidden')) {
|
||||||
|
document.getElementById('inline-progress-fill').style.width = pct;
|
||||||
|
document.getElementById('inline-progress-label').textContent = label;
|
||||||
|
const inlineStage = document.getElementById('inline-progress-stage');
|
||||||
|
if (r.data.stageName && r.data.stageCount > 1) {
|
||||||
|
inlineStage.textContent = tr('install.step', null, {current: String(r.data.stageIndex || 0), total: String(r.data.stageCount), name: r.data.stageName});
|
||||||
|
inlineStage.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
inlineStage.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalStage = document.getElementById('progress-stage');
|
||||||
if (r.data.stageName && r.data.stageCount > 1) {
|
if (r.data.stageName && r.data.stageCount > 1) {
|
||||||
stageInfo.textContent = tr('install.step', null, {current: String(r.data.stageIndex || 0), total: String(r.data.stageCount), name: r.data.stageName});
|
modalStage.textContent = tr('install.step', null, {current: String(r.data.stageIndex || 0), total: String(r.data.stageCount), name: r.data.stageName});
|
||||||
stageInfo.classList.remove('hidden');
|
modalStage.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
stageInfo.classList.add('hidden');
|
modalStage.classList.add('hidden');
|
||||||
}
|
}
|
||||||
if (!r.data.inProgress) {
|
if (!r.data.inProgress) {
|
||||||
this.stopProgressPoll();
|
this.stopProgressPoll();
|
||||||
this.hideInstallModal();
|
this.hideInstallModal();
|
||||||
|
inlineProgress.classList.add('hidden');
|
||||||
var failed = r.data.stageName === 'Failed' || r.data.stageName === 'Error' || (r.data.label && r.data.label.startsWith('Error'));
|
var failed = r.data.stageName === 'Failed' || r.data.stageName === 'Error' || (r.data.label && r.data.label.startsWith('Error'));
|
||||||
this.toast(failed ? (r.data.label || t('toast.installFailed')) : t('toast.installComplete'), failed ? 'error' : 'success');
|
this.toast(failed ? (r.data.label || t('toast.installFailed')) : t('toast.installComplete'), failed ? 'error' : 'success');
|
||||||
if (!failed) await this.loadInstances();
|
if (!failed) {
|
||||||
|
await this.loadInstances();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ body {
|
|||||||
.select-wrap select:focus { border-color: var(--accent); }
|
.select-wrap select:focus { border-color: var(--accent); }
|
||||||
|
|
||||||
.install-progress { padding-top: 16px; border-top: 1px solid var(--border); }
|
.install-progress { padding-top: 16px; border-top: 1px solid var(--border); }
|
||||||
|
.inline-install-progress { padding: 16px 20px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||||
.progress-track {
|
.progress-track {
|
||||||
height: 6px; background: var(--bg-surface); border-radius: 3px; overflow: hidden;
|
height: 6px; background: var(--bg-surface); border-radius: 3px; overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -881,3 +882,8 @@ body {
|
|||||||
.admin-news-info { flex: 1; min-width: 0; }
|
.admin-news-info { flex: 1; min-width: 0; }
|
||||||
.admin-news-title { font-size: 13px; font-weight: 500; }
|
.admin-news-title { font-size: 13px; font-weight: 500; }
|
||||||
.admin-news-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
.admin-news-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
||||||
|
|
||||||
|
.pack-entry.disabled { opacity: 0.5; pointer-events: none; }
|
||||||
|
.pack-entry.disabled .pack-entry-name::after { content: ' (' attr(data-disabled-text) ')'; color: var(--error); font-size: 11px; }
|
||||||
|
.admin-pack-disabled { color: var(--error) !important; font-weight: 600; }
|
||||||
|
.admin-pack-enabled { color: var(--success) !important; font-weight: 600; }
|
||||||
|
|||||||
+5
-18
@@ -72,31 +72,31 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-web</artifactId>
|
<artifactId>javafx-web</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-graphics</artifactId>
|
<artifactId>javafx-graphics</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-base</artifactId>
|
<artifactId>javafx-base</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-media</artifactId>
|
<artifactId>javafx-media</artifactId>
|
||||||
<version>21</version>
|
<version>23</version>
|
||||||
<classifier>win</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
@@ -142,27 +142,14 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
<profiles>
|
<profiles>
|
||||||
<!-- ==================== GLOBAL ==================== -->
|
|
||||||
<profile>
|
<profile>
|
||||||
<id>global</id>
|
<id>global</id>
|
||||||
<activation>
|
<activation>
|
||||||
<activeByDefault>true</activeByDefault>
|
<activeByDefault>true</activeByDefault>
|
||||||
</activation>
|
</activation>
|
||||||
<properties>
|
<properties>
|
||||||
<build.profile>global</build.profile>
|
|
||||||
<launcher.title>ZernMC Launcher</launcher.title>
|
<launcher.title>ZernMC Launcher</launcher.title>
|
||||||
<server.url>http://87.120.187.36:1582</server.url>
|
<server.url>http://87.120.187.36:1582</server.url>
|
||||||
<!-- Можно добавить флаги для отключения некоторых фич -->
|
|
||||||
</properties>
|
|
||||||
</profile>
|
|
||||||
|
|
||||||
<!-- ==================== ZERNMC ==================== -->
|
|
||||||
<profile>
|
|
||||||
<id>zernmc</id>
|
|
||||||
<properties>
|
|
||||||
<build.profile>zernmc</build.profile>
|
|
||||||
<launcher.title>ZernMC Private Launcher</launcher.title>
|
|
||||||
<server.url>http://87.120.187.36:1582</server.url>
|
|
||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import hashlib
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pack_manager import PACKS_DIR, load_packs_config, get_pack_presets, toggle_pack_disabled
|
||||||
|
|
||||||
from auth import get_db, require_role, log_audit, get_current_user, hash_password
|
from auth import get_db, require_role, log_audit, get_current_user, hash_password
|
||||||
from roles import (
|
from roles import (
|
||||||
ROLE_PERMISSIONS, UserRole, ROLE_NAMES, has_permission, Permissions,
|
ROLE_PERMISSIONS, UserRole, ROLE_NAMES, has_permission, Permissions,
|
||||||
@@ -877,6 +879,58 @@ async def admin_delete_news(
|
|||||||
return {"success": True, "id": target.stem}
|
return {"success": True, "id": target.stem}
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== PACK MANAGEMENT (ELDER+) ======================
|
||||||
|
|
||||||
|
@router.get("/packs")
|
||||||
|
async def admin_list_packs(
|
||||||
|
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||||
|
):
|
||||||
|
"""List all packs with disabled status"""
|
||||||
|
packs = []
|
||||||
|
packs_config = load_packs_config()
|
||||||
|
|
||||||
|
packs_dir = Path("packs")
|
||||||
|
if packs_dir.exists():
|
||||||
|
for pack_dir in packs_dir.iterdir():
|
||||||
|
if pack_dir.is_dir():
|
||||||
|
entry = packs_config.get(pack_dir.name, {})
|
||||||
|
packs.append({
|
||||||
|
"name": pack_dir.name,
|
||||||
|
"disabled": entry.get("disabled", False),
|
||||||
|
"presets": get_pack_presets(pack_dir.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"packs": packs}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/packs/{pack_name}/toggle")
|
||||||
|
async def admin_toggle_pack(
|
||||||
|
pack_name: str,
|
||||||
|
current_user: dict = Depends(require_role(ROLE_ELDER)),
|
||||||
|
request: Request = None,
|
||||||
|
):
|
||||||
|
"""Toggle pack disabled/enabled status"""
|
||||||
|
pack_path = PACKS_DIR / pack_name
|
||||||
|
if not pack_path.exists() or not pack_path.is_dir():
|
||||||
|
raise HTTPException(404, "Pack not found")
|
||||||
|
|
||||||
|
new_state = toggle_pack_disabled(pack_name)
|
||||||
|
|
||||||
|
ip = request.client.host if request.client else "unknown"
|
||||||
|
log_audit(
|
||||||
|
current_user["id"],
|
||||||
|
"pack_toggle",
|
||||||
|
f"{'Disabled' if new_state else 'Enabled'} pack {pack_name}",
|
||||||
|
ip
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"pack_name": pack_name,
|
||||||
|
"disabled": new_state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def get_my_info(current_user: dict = Depends(get_current_user)):
|
async def get_my_info(current_user: dict = Depends(get_current_user)):
|
||||||
"""Информация о текущем пользователе с правами"""
|
"""Информация о текущем пользователе с правами"""
|
||||||
|
|||||||
+49
-10
@@ -19,7 +19,7 @@ from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
|||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
|
|
||||||
from pack_manager import DATA_DIR, scan_pack, get_cached_manifest, PACKS_DIR
|
from pack_manager import DATA_DIR, scan_pack, get_cached_manifest, PACKS_DIR, is_pack_disabled, get_pack_presets, get_merged_manifest
|
||||||
from models import PackMeta
|
from models import PackMeta
|
||||||
from middleware import LoggingMiddleware
|
from middleware import LoggingMiddleware
|
||||||
from cli import parse_args, run_test_mode, run_production_mode, run_development_mode, run_sync_mode
|
from cli import parse_args, run_test_mode, run_production_mode, run_development_mode, run_sync_mode
|
||||||
@@ -1026,7 +1026,9 @@ async def list_packs(
|
|||||||
"loader_type": meta.get("loader_type", "vanilla"),
|
"loader_type": meta.get("loader_type", "vanilla"),
|
||||||
"loader_version": meta.get("loader_version"),
|
"loader_version": meta.get("loader_version"),
|
||||||
"asset_index": meta.get("asset_index"),
|
"asset_index": meta.get("asset_index"),
|
||||||
"description": description
|
"description": description,
|
||||||
|
"disabled": is_pack_disabled(pack_dir.name),
|
||||||
|
"presets": get_pack_presets(pack_dir.name)
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load pack meta for {pack_dir.name}: {e}")
|
logger.error(f"Failed to load pack meta for {pack_dir.name}: {e}")
|
||||||
@@ -1047,7 +1049,8 @@ async def list_packs(
|
|||||||
async def get_pack_diff(
|
async def get_pack_diff(
|
||||||
pack_name: str,
|
pack_name: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: dict = Depends(get_current_user) # Добавляем зависимость
|
current_user: dict = Depends(get_current_user),
|
||||||
|
preset: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""Client sends: { "mods/jei.jar": "sha256_hash", ... }
|
"""Client sends: { "mods/jei.jar": "sha256_hash", ... }
|
||||||
Server returns diff information
|
Server returns diff information
|
||||||
@@ -1060,6 +1063,10 @@ async def get_pack_diff(
|
|||||||
detail="Для скачивания сборок требуется активная проходка. Обратитесь к администратору."
|
detail="Для скачивания сборок требуется активная проходка. Обратитесь к администратору."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if pack is disabled
|
||||||
|
if is_pack_disabled(pack_name):
|
||||||
|
raise HTTPException(403, "Pack is disabled by administrator")
|
||||||
|
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
# Читаем тело запроса
|
# Читаем тело запроса
|
||||||
@@ -1071,13 +1078,17 @@ async def get_pack_diff(
|
|||||||
|
|
||||||
logger.info("Received diff request",
|
logger.info("Received diff request",
|
||||||
pack=pack_name,
|
pack=pack_name,
|
||||||
|
preset=preset,
|
||||||
client_files_count=len(body),
|
client_files_count=len(body),
|
||||||
client_ip=client_ip)
|
client_ip=client_ip)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
meta = get_cached_manifest(pack_name)
|
if preset:
|
||||||
if not meta:
|
meta = get_merged_manifest(pack_name, preset)
|
||||||
meta = await scan_pack(pack_name)
|
else:
|
||||||
|
meta = get_cached_manifest(pack_name)
|
||||||
|
if not meta:
|
||||||
|
meta = await scan_pack(pack_name)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning("Pack not found", pack=pack_name, client_ip=client_ip)
|
logger.warning("Pack not found", pack=pack_name, client_ip=client_ip)
|
||||||
raise HTTPException(404, "Pack not found")
|
raise HTTPException(404, "Pack not found")
|
||||||
@@ -1095,6 +1106,8 @@ async def get_pack_diff(
|
|||||||
client_hash = body.get(path)
|
client_hash = body.get(path)
|
||||||
if client_hash is None or client_hash != entry.hash:
|
if client_hash is None or client_hash != entry.hash:
|
||||||
url = f"/pack/{pack_name}/file/{path}"
|
url = f"/pack/{pack_name}/file/{path}"
|
||||||
|
if preset:
|
||||||
|
url += f"?preset={preset}"
|
||||||
to_download.append({
|
to_download.append({
|
||||||
"path": path,
|
"path": path,
|
||||||
"url": url,
|
"url": url,
|
||||||
@@ -1127,6 +1140,9 @@ async def get_pack_diff(
|
|||||||
@app.get("/pack/{pack_name}")
|
@app.get("/pack/{pack_name}")
|
||||||
async def get_pack_manifest(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)):
|
async def get_pack_manifest(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)):
|
||||||
"""Get pack manifest with caching"""
|
"""Get pack manifest with caching"""
|
||||||
|
if is_pack_disabled(pack_name):
|
||||||
|
raise HTTPException(403, "Pack is disabled by administrator")
|
||||||
|
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
cached_meta = get_cached_manifest(pack_name)
|
cached_meta = get_cached_manifest(pack_name)
|
||||||
@@ -1166,13 +1182,34 @@ async def get_pack_manifest(pack_name: str, request: Request, current_user: dict
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/pack/{pack_name}/file/{file_path:path}")
|
@app.get("/pack/{pack_name}/file/{file_path:path}")
|
||||||
async def get_pack_file(pack_name: str, file_path: str, request: Request, current_user: dict = Depends(get_current_user)):
|
async def get_pack_file(pack_name: str, file_path: str, request: Request, current_user: dict = Depends(get_current_user), preset: Optional[str] = None):
|
||||||
"""Get a file from a pack"""
|
"""Get a file from a pack. If preset is specified, try preset dir first, fall back to base."""
|
||||||
if not has_permission(current_user["role"], Permissions.DOWNLOAD_PACK):
|
if not has_permission(current_user["role"], Permissions.DOWNLOAD_PACK):
|
||||||
raise HTTPException(403, "Requires active pass")
|
raise HTTPException(403, "Requires active pass")
|
||||||
full_path = PACKS_DIR / pack_name / file_path
|
if is_pack_disabled(pack_name):
|
||||||
|
raise HTTPException(403, "Pack is disabled by administrator")
|
||||||
|
|
||||||
client_ip = request.client.host if request.client else None
|
client_ip = request.client.host if request.client else None
|
||||||
|
|
||||||
|
# If preset specified, try preset path first
|
||||||
|
if preset:
|
||||||
|
preset_path = PACKS_DIR / pack_name / "presets" / preset / file_path
|
||||||
|
preset_resolved = preset_path.resolve()
|
||||||
|
preset_root = (PACKS_DIR / pack_name).resolve()
|
||||||
|
try:
|
||||||
|
if str(preset_resolved).startswith(str(preset_root)) and preset_resolved.is_file():
|
||||||
|
full_path = preset_resolved
|
||||||
|
else:
|
||||||
|
full_path = None
|
||||||
|
except (ValueError, OSError):
|
||||||
|
full_path = None
|
||||||
|
else:
|
||||||
|
full_path = None
|
||||||
|
|
||||||
|
# Fall back to base path
|
||||||
|
if full_path is None:
|
||||||
|
full_path = PACKS_DIR / pack_name / file_path
|
||||||
|
|
||||||
# Security: prevent path traversal
|
# Security: prevent path traversal
|
||||||
try:
|
try:
|
||||||
full_path = full_path.resolve()
|
full_path = full_path.resolve()
|
||||||
@@ -2245,7 +2282,9 @@ async def proxy_download(request: Request):
|
|||||||
"resources.download.minecraft.net",
|
"resources.download.minecraft.net",
|
||||||
"maven.minecraftforge.net",
|
"maven.minecraftforge.net",
|
||||||
"files.minecraftforge.net",
|
"files.minecraftforge.net",
|
||||||
"maven.neoforged.net"
|
"maven.neoforged.net",
|
||||||
|
"api.zernmc.ru",
|
||||||
|
"api.zernmc.online"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Проверяем, что URL ведет на разрешенный домен
|
# Проверяем, что URL ведет на разрешенный домен
|
||||||
|
|||||||
+15
-12
@@ -154,24 +154,27 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|||||||
_stats["blocked"] += 1
|
_stats["blocked"] += 1
|
||||||
return Response(status_code=404, content="")
|
return Response(status_code=404, content="")
|
||||||
|
|
||||||
# Check rate limit
|
path = request.url.path
|
||||||
if not check_rate_limit(client_ip):
|
|
||||||
_stats["rate_limited"] += 1
|
# Skip rate limiting for pack file downloads (direct high-speed serving)
|
||||||
# Periodic stats logging instead of every warning
|
if path.startswith("/pack/") and "/file/" in path:
|
||||||
if time.time() - _stats_last_log > STATS_LOG_INTERVAL:
|
is_file_download = True
|
||||||
logger.warning(f"Stats: {_stats}")
|
else:
|
||||||
_stats_last_log = time.time()
|
is_file_download = False
|
||||||
return Response(status_code=429, content="Too many requests")
|
|
||||||
|
# Check rate limit
|
||||||
|
if not check_rate_limit(client_ip):
|
||||||
|
_stats["rate_limited"] += 1
|
||||||
|
if time.time() - _stats_last_log > STATS_LOG_INTERVAL:
|
||||||
|
logger.warning(f"Stats: {_stats}")
|
||||||
|
_stats_last_log = time.time()
|
||||||
|
return Response(status_code=429, content="Too many requests")
|
||||||
|
|
||||||
# Check suspicious path (silent 404 for bots)
|
# Check suspicious path (silent 404 for bots)
|
||||||
path = request.url.path
|
|
||||||
if is_suspicious_path(path):
|
if is_suspicious_path(path):
|
||||||
# Return 404 without logging - confuse the bots
|
# Return 404 without logging - confuse the bots
|
||||||
return Response(status_code=404, content="")
|
return Response(status_code=404, content="")
|
||||||
|
|
||||||
# Skip logging for large file downloads (don't spam logs)
|
|
||||||
is_file_download = path.startswith("/pack/") and "/file/" in path
|
|
||||||
|
|
||||||
# Track total requests for stats
|
# Track total requests for stats
|
||||||
_stats["total"] += 1
|
_stats["total"] += 1
|
||||||
|
|
||||||
|
|||||||
+104
-4
@@ -1,9 +1,10 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict, List
|
||||||
import structlog
|
import structlog
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiofiles
|
import aiofiles
|
||||||
@@ -19,6 +20,46 @@ DATA_DIR.mkdir(exist_ok=True)
|
|||||||
# Cache for loaded manifests
|
# Cache for loaded manifests
|
||||||
_manifest_cache: Dict[str, PackMeta] = {}
|
_manifest_cache: Dict[str, PackMeta] = {}
|
||||||
|
|
||||||
|
# Pack config cache
|
||||||
|
_packs_config: Optional[Dict] = None
|
||||||
|
|
||||||
|
def load_packs_config() -> dict:
|
||||||
|
global _packs_config
|
||||||
|
if _packs_config is not None:
|
||||||
|
return _packs_config
|
||||||
|
config_path = DATA_DIR / "packs_config.json"
|
||||||
|
if config_path.exists():
|
||||||
|
try:
|
||||||
|
_packs_config = json.loads(config_path.read_text())
|
||||||
|
except Exception:
|
||||||
|
_packs_config = {}
|
||||||
|
else:
|
||||||
|
_packs_config = {}
|
||||||
|
return _packs_config
|
||||||
|
|
||||||
|
def save_packs_config(config: dict):
|
||||||
|
global _packs_config
|
||||||
|
_packs_config = config
|
||||||
|
config_path = DATA_DIR / "packs_config.json"
|
||||||
|
config_path.write_text(json.dumps(config, indent=2))
|
||||||
|
|
||||||
|
def is_pack_disabled(pack_name: str) -> bool:
|
||||||
|
config = load_packs_config()
|
||||||
|
return config.get(pack_name, {}).get("disabled", False)
|
||||||
|
|
||||||
|
def toggle_pack_disabled(pack_name: str) -> bool:
|
||||||
|
config = load_packs_config()
|
||||||
|
entry = config.setdefault(pack_name, {})
|
||||||
|
entry["disabled"] = not entry.get("disabled", False)
|
||||||
|
save_packs_config(config)
|
||||||
|
return entry["disabled"]
|
||||||
|
|
||||||
|
def get_pack_presets(pack_name: str) -> List[str]:
|
||||||
|
presets_dir = PACKS_DIR / pack_name / "presets"
|
||||||
|
if not presets_dir.exists() or not presets_dir.is_dir():
|
||||||
|
return []
|
||||||
|
return sorted([d.name for d in presets_dir.iterdir() if d.is_dir()])
|
||||||
|
|
||||||
def get_cached_manifest(pack_name: str) -> Optional[PackMeta]:
|
def get_cached_manifest(pack_name: str) -> Optional[PackMeta]:
|
||||||
"""Get manifest from memory cache if available"""
|
"""Get manifest from memory cache if available"""
|
||||||
return _manifest_cache.get(pack_name)
|
return _manifest_cache.get(pack_name)
|
||||||
@@ -66,10 +107,11 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
new_files: Dict[str, FileEntry] = {}
|
new_files: Dict[str, FileEntry] = {}
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
# Get ignored directories
|
# Get ignored directories (always use current list, not cached from meta)
|
||||||
ignored_dirs = current_meta.ignored_dirs if current_meta else [
|
ignored_dirs = [
|
||||||
"resourcepacks", "shaderpacks", "saves", "logs",
|
"resourcepacks", "shaderpacks", "saves", "logs",
|
||||||
"crash-reports", "screenshots", "journeymap", "config"
|
"crash-reports", "screenshots", "journeymap",
|
||||||
|
"presets"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Walk through pack directory
|
# Walk through pack directory
|
||||||
@@ -81,10 +123,19 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
file_path = Path(root) / file
|
file_path = Path(root) / file
|
||||||
rel_path = file_path.relative_to(pack_path).as_posix()
|
rel_path = file_path.relative_to(pack_path).as_posix()
|
||||||
|
|
||||||
|
# Skip metadata files that should not be part of the pack
|
||||||
|
if file in ("description.txt", "instance.json"):
|
||||||
|
continue
|
||||||
|
|
||||||
# Skip if in ignored directory
|
# Skip if in ignored directory
|
||||||
if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored_dirs):
|
if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored_dirs):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Validate .jar files — skip corrupted ones
|
||||||
|
if file.endswith(".jar") and not _is_valid_zip(file_path):
|
||||||
|
logger.warning(f"Skipping corrupt jar in pack {pack_name}: {rel_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
file_hash = await calculate_sha256(file_path)
|
file_hash = await calculate_sha256(file_path)
|
||||||
|
|
||||||
@@ -154,3 +205,52 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
|
|
||||||
# Should not happen
|
# Should not happen
|
||||||
raise Exception(f"Failed to scan pack {pack_name}")
|
raise Exception(f"Failed to scan pack {pack_name}")
|
||||||
|
|
||||||
|
def _is_valid_zip(path: Path) -> bool:
|
||||||
|
"""Check if a file is a valid ZIP archive"""
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(path, 'r') as zf:
|
||||||
|
return zf.testzip() is None
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_merged_manifest(pack_name: str, preset: str) -> PackMeta:
|
||||||
|
"""Get manifest merged with preset files. Preset files override base files."""
|
||||||
|
base_meta = get_cached_manifest(pack_name)
|
||||||
|
if not base_meta:
|
||||||
|
raise FileNotFoundError(f"Pack {pack_name} not found")
|
||||||
|
|
||||||
|
preset_dir = PACKS_DIR / pack_name / "presets" / preset
|
||||||
|
if not preset_dir.exists() or not preset_dir.is_dir():
|
||||||
|
raise FileNotFoundError(f"Preset {preset} not found for pack {pack_name}")
|
||||||
|
|
||||||
|
merged_files = dict(base_meta.files)
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(preset_dir):
|
||||||
|
for file in files:
|
||||||
|
file_path = Path(root) / file
|
||||||
|
rel_path = file_path.relative_to(preset_dir).as_posix()
|
||||||
|
stat = file_path.stat()
|
||||||
|
file_hash = calculate_sha256_sync(file_path)
|
||||||
|
merged_files[rel_path] = FileEntry(
|
||||||
|
path=rel_path,
|
||||||
|
hash=file_hash,
|
||||||
|
size=stat.st_size,
|
||||||
|
added_at=datetime.utcfromtimestamp(stat.st_ctime),
|
||||||
|
modified_at=datetime.utcfromtimestamp(stat.st_mtime)
|
||||||
|
)
|
||||||
|
|
||||||
|
return PackMeta(
|
||||||
|
pack_name=base_meta.pack_name,
|
||||||
|
version=base_meta.version,
|
||||||
|
files=merged_files,
|
||||||
|
updated_at=base_meta.updated_at,
|
||||||
|
ignored_dirs=base_meta.ignored_dirs,
|
||||||
|
minecraft_version=base_meta.minecraft_version,
|
||||||
|
loader_type=base_meta.loader_type,
|
||||||
|
loader_version=base_meta.loader_version,
|
||||||
|
asset_index=base_meta.asset_index
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user