diff --git a/core/src/main/java/pl/skidam/automodpack_core/Constants.java b/core/src/main/java/pl/skidam/automodpack_core/Constants.java index af8a66b2e..a7acd7db7 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/Constants.java +++ b/core/src/main/java/pl/skidam/automodpack_core/Constants.java @@ -1,5 +1,6 @@ package pl.skidam.automodpack_core; +import java.nio.file.Path; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import pl.skidam.automodpack_core.config.Jsons; @@ -7,11 +8,10 @@ import pl.skidam.automodpack_core.modpack.ModpackExecutor; import pl.skidam.automodpack_core.protocol.netty.NettyServer; -import java.nio.file.Path; - // More or less constants // TODO cleanup public class Constants { + public static final Logger LOGGER = LogManager.getLogger("AutoModpack"); public static final String MOD_ID = "automodpack"; // For real its "automodpack_mod" but we use this for resource locations etc. public static Boolean DEBUG = false; @@ -28,8 +28,8 @@ public class Constants { public static Path MODS_DIR; public static ModpackExecutor modpackExecutor; public static NettyServer hostServer; - public static Jsons.ServerConfigFieldsV2 serverConfig; - public static Jsons.ClientConfigFieldsV2 clientConfig; + public static Jsons.ServerConfigFieldsV3 serverConfig; + public static Jsons.ClientConfigFieldsV3 clientConfig; public static Jsons.KnownHostsFields knownHosts; public static final Path automodpackDir = Path.of("automodpack"); public static final Path storeDir = automodpackDir.resolve("store"); @@ -38,10 +38,8 @@ public class Constants { // Main - required // Addons - optional addon packs // Switches - optional or required packs, chosen by the player, only one can be installed at a time - public static final Path hostContentModpackDir = hostModpackDir.resolve("main"); public static Path hostModpackContentFile = hostModpackDir.resolve("automodpack-content.json"); public static Path serverConfigFile = automodpackDir.resolve("automodpack-server.json"); - public static Path clientLocalMetadataFile = automodpackDir.resolve("automodpack-client-metadata.json"); public static Path cacheDir = automodpackDir.resolve("cache"); public static Path hashCacheDBFile = cacheDir.resolve("hash-cache.db"); public static Path modCacheDBFile = cacheDir.resolve("mod-cache.db"); @@ -54,10 +52,10 @@ public class Constants { public static final Path serverCertFile = privateDir.resolve("cert.crt"); public static final Path serverPrivateKeyFile = privateDir.resolve("key.pem"); - // Client public static final Path modpackContentTempFile = automodpackDir.resolve("automodpack-content.json.temp"); public static final Path clientConfigFile = automodpackDir.resolve("automodpack-client.json"); + public static final Path clientSelectionFile = automodpackDir.resolve("automodpack-client-selection.json"); public static final Path clientSecretsFile = privateDir.resolve("automodpack-client-secrets.json"); public static final Path modpacksDir = automodpackDir.resolve("modpacks"); diff --git a/core/src/main/java/pl/skidam/automodpack_core/Server.java b/core/src/main/java/pl/skidam/automodpack_core/Server.java index a1993b433..67f60a5f1 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/Server.java +++ b/core/src/main/java/pl/skidam/automodpack_core/Server.java @@ -1,21 +1,21 @@ package pl.skidam.automodpack_core; +import static pl.skidam.automodpack_core.Constants.*; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Set; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.modpack.ModpackExecutor; import pl.skidam.automodpack_core.modpack.ModpackContent; +import pl.skidam.automodpack_core.modpack.ModpackExecutor; import pl.skidam.automodpack_core.protocol.netty.NettyServer; -import java.nio.file.Path; -import java.util.HashSet; - -import static pl.skidam.automodpack_core.Constants.*; - public class Server { // TODO Finish this class that it will be able to host the server without mod public static void main(String[] args) { - if (args.length < 1) { LOGGER.error("Modpack id not provided!"); return; @@ -35,9 +35,8 @@ public static void main(String[] args) { serverConfigFile = modpackDir.resolve("automodpack-server.json"); serverCoreConfigFile = modpackDir.resolve("automodpack-core.json"); - serverConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV2.class); + serverConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV3.class); if (serverConfig != null) { - serverConfig.syncedFiles = new HashSet<>(); serverConfig.validateSecrets = false; ConfigTools.save(serverConfigFile, serverConfig); @@ -60,8 +59,7 @@ public static void main(String[] args) { mainModpackDir.toFile().mkdirs(); ModpackExecutor modpackExecutor = new ModpackExecutor(); - ModpackContent modpackContent = new ModpackContent(serverConfig.modpackName, null, mainModpackDir, serverConfig.syncedFiles, serverConfig.allowEditsInFiles, serverConfig.forceCopyFilesToStandardLocation, modpackExecutor.getExecutor()); - boolean generated = modpackExecutor.generateNew(modpackContent); + boolean generated = modpackExecutor.generateNew(); if (generated) { LOGGER.info("Modpack generated!"); diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java index 359b8c003..c41ce554b 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigTools.java @@ -1,26 +1,21 @@ - package pl.skidam.automodpack_core.config; -import com.google.gson.*; -import pl.skidam.automodpack_core.utils.AddressHelpers; +import static pl.skidam.automodpack_core.Constants.*; +import com.google.gson.*; import java.lang.reflect.Type; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; - -import static pl.skidam.automodpack_core.Constants.*; +import pl.skidam.automodpack_core.utils.AddressHelpers; public class ConfigTools { - public static Gson GSON = new GsonBuilder() - .disableHtmlEscaping() - .setPrettyPrinting() - .registerTypeAdapter(InetSocketAddress.class, new InetSocketAddressTypeAdapter()) - .create(); + public static Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().registerTypeAdapter(InetSocketAddress.class, new InetSocketAddressTypeAdapter()).create(); + + private static class InetSocketAddressTypeAdapter implements JsonSerializer, JsonDeserializer { - private static class InetSocketAddressTypeAdapter implements JsonSerializer,JsonDeserializer { @Override public JsonElement serialize(InetSocketAddress src, Type typeOfSrc, JsonSerializationContext context) { return new JsonPrimitive(src.getHostString() + ":" + src.getPort()); @@ -50,14 +45,14 @@ public static T softLoad(Path configFile, Class configClass) { String json = Files.readString(configFile); return GSON.fromJson(json, configClass); } - } catch (Exception ignored) { } + } catch (Exception ignored) {} return null; } public static T load(Path configFile, Class configClass) { try { if (!Files.isDirectory(configFile.getParent())) { - Files.createDirectories(configFile.getParent()); + Files.createDirectories(configFile.getParent()); } if (Files.isRegularFile(configFile)) { @@ -80,7 +75,8 @@ public static T load(Path configFile, Class configClass) { e.printStackTrace(); } - try { // create new config + try { + // create new config T obj = getConfigObject(configClass); save(configFile, obj); return obj; @@ -94,7 +90,7 @@ public static T load(Path configFile, Class configClass) { public static T load(String json, Class configClass) { try { if (json != null) { - return GSON.fromJson(json, configClass); + return GSON.fromJson(json, configClass); } } catch (Exception e) { LOGGER.error("Couldn't load config! " + configClass); @@ -121,13 +117,12 @@ public static void save(Path configFile, Object configObject) { } } - // Modpack content stuff - public static Jsons.ModpackContentFields loadModpackContent(Path modpackContentFile) { + public static Jsons.ModpackContent loadModpackContent(Path modpackContentFile) { try { if (Files.isRegularFile(modpackContentFile)) { String json = Files.readString(modpackContentFile); - return GSON.fromJson(json, Jsons.ModpackContentFields.class); + return GSON.fromJson(json, Jsons.ModpackContent.class); } } catch (Exception e) { LOGGER.error("Couldn't load modpack content! {}", modpackContentFile.toAbsolutePath().normalize(), e); @@ -135,7 +130,7 @@ public static Jsons.ModpackContentFields loadModpackContent(Path modpackContentF return null; } - public static void saveModpackContent(Path modpackContentFile, Jsons.ModpackContentFields configObject) { + public static void saveModpackContent(Path modpackContentFile, Jsons.ModpackContent configObject) { try { if (!Files.isDirectory(modpackContentFile.getParent())) { Files.createDirectories(modpackContentFile.getParent()); @@ -147,4 +142,33 @@ public static void saveModpackContent(Path modpackContentFile, Jsons.ModpackCont e.printStackTrace(); } } + + public static Jsons.ClientSelectionManagerFields loadClientSelectionManager(Path selectionFile) { + try { + if (Files.isRegularFile(selectionFile)) { + String json = Files.readString(selectionFile); + Jsons.ClientSelectionManagerFields obj = GSON.fromJson(json, Jsons.ClientSelectionManagerFields.class); + if (obj == null) { + return new Jsons.ClientSelectionManagerFields(); + } + return obj; + } + } catch (Exception e) { + LOGGER.debug("Couldn't load client selection manager file (this is normal on first startup): {}", e.getMessage()); + } + return new Jsons.ClientSelectionManagerFields(); + } + + public static void saveClientSelectionManager(Path selectionFile, Jsons.ClientSelectionManagerFields configObject) { + try { + if (!Files.isDirectory(selectionFile.getParent())) { + Files.createDirectories(selectionFile.getParent()); + } + + Files.writeString(selectionFile, GSON.toJson(configObject), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (Exception e) { + LOGGER.error("Couldn't save client selection manager!"); + e.printStackTrace(); + } + } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigUtils.java b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigUtils.java index 11b7f58be..3894b5d8e 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/ConfigUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/ConfigUtils.java @@ -14,6 +14,7 @@ public static void normalizeServerConfig(Jsons.ServerConfigFieldsV2 config, bool } } + // TODO adapt yourself public static void normalizeServerConfig(Jsons.ServerConfigFieldsV2 config) { Set fixedSyncedFiles = new HashSet<>(config.syncedFiles.size()); Set fixedAllowEditsInFiles = new HashSet<>(config.allowEditsInFiles.size()); diff --git a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java index 09867ce4b..a1cb5b4ac 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java +++ b/core/src/main/java/pl/skidam/automodpack_core/config/Jsons.java @@ -1,11 +1,9 @@ - package pl.skidam.automodpack_core.config; -import pl.skidam.automodpack_core.auth.Secrets; - import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import pl.skidam.automodpack_core.auth.Secrets; @SuppressWarnings("unused") public class Jsons { @@ -33,6 +31,17 @@ public static class ClientConfigFieldsV2 { public boolean allowRemoteNonModpackDeletions = true; } + public static class ClientConfigFieldsV3 { + public int DO_NOT_CHANGE_IT = 3; // file version + public boolean updateSelectedModpackOnLaunch = true; + public boolean selfUpdater = false; + public boolean syncAutoModpackVersion = true; + public boolean syncLoaderVersion = true; + public boolean playMusic = true; + public boolean allowRemoteNonModpackDeletions = true; + } + + public static class ModpackAddresses { public InetSocketAddress hostAddress; // modpack host address public InetSocketAddress serverAddress; // minecraft server address @@ -56,7 +65,7 @@ public ModpackAddresses(InetSocketAddress hostAddress, InetSocketAddress serverA } public boolean isAnyEmpty() { - return hostAddress == null || serverAddress == null || hostAddress.getHostString().isBlank() || serverAddress.getHostString().isBlank(); + return (hostAddress == null || serverAddress == null || hostAddress.getHostString().isBlank() || serverAddress.getHostString().isBlank()); } } @@ -115,16 +124,70 @@ public static class ServerConfigFieldsV2 { public long secretLifetime = 336; // 336 hours = 14 days public boolean selfUpdater = false; public Set acceptedLoaders = new HashSet<>(); + } - public static class FileToDelete { // Same as in ModpackContentFields.FileToDelete but without timestamp - public final String file; - public final String sha1; + public static class ServerConfigFieldsV3 { + public int DO_NOT_CHANGE_IT = 3; // file version + public String modpackName = ""; + public boolean modpackHost = true; + public boolean generateModpackOnStart = true; + public Map groups = Map.of( + "main", mainGroupDeclaration() + ); + public Map nonModpackFilesToDelete = Map.of(); // FileToDelete but without timestamp + public boolean autoExcludeServerSideMods = true; + public boolean autoExcludeUnnecessaryFiles = true; + public boolean requireAutoModpackOnClient = true; + public boolean nagUnModdedClients = true; + public String nagMessage = "This server provides dedicated modpack through AutoModpack!"; + public String nagClickableMessage = "Click here to get the AutoModpack!"; + public String nagClickableLink = "https://modrinth.com/project/automodpack"; + public String bindAddress = ""; + public int bindPort = -1; + public String addressToSend = ""; + public int portToSend = -1; + public boolean disableInternalTLS = false; + public boolean requireMagicPackets = false; + public boolean updateIpsOnEveryStart = false; + public int bandwidthLimit = 0; + public boolean validateSecrets = true; + public long secretLifetime = 336; // 336 hours = 14 days + public boolean selfUpdater = false; + public Set acceptedLoaders = new HashSet<>(); + } - public FileToDelete(String file, String sha1) { - this.file = file; - this.sha1 = sha1; - } - } + public static GroupDeclaration mainGroupDeclaration() { + GroupDeclaration decl = new GroupDeclaration(); + decl.required = true; + decl.recommended = true; + decl.syncedFiles = List.of("/mods/*.jar", "/kubejs/**", "!/kubejs/server_scripts/**", "/emotes/*"); + decl.allowEditsInFiles = List.of("/options.txt", "/config/**"); + return decl; + } + + // TODO see that we changed set to list for + public static class GroupDeclaration { + + // UI Metadata + public String displayName = ""; // Its already as a map key (group id / file path) but the visible might me different + public String description = ""; // e.g., "Increases FPS using Sodium/Lithium" + public String category = ""; + + // Logic Flags + public boolean required = false; // If true, user cannot uncheck (with the question above it seems that if client may opt for a different required pack if both are breaking each other) + public boolean recommended = false; // Selected by default, if required this option doesn't matter + + // Dependency & Compatibility + public List breaksWith = List.of(); // e.g., ["optifine-group"] + public List requires = List.of(); // e.g., ["fabric-api-group"] + + // OS Compatibility (Enum: WINDOWS, LINUX, MACOS, ANDROID) + public List compatibleOS = List.of(); // Empty = All OS + + // File Scanning Rules (Per Group) + public List syncedFiles = List.of(); // Relative to group folder + public List allowEditsInFiles = List.of(); + public List forceCopyFilesToStandardLocation = List.of(); } public static class ServerCoreConfigFields { @@ -135,90 +198,25 @@ public static class ServerCoreConfigFields { } public static class SecretsFields { + public Map secrets = new HashMap<>(); } public static class KnownHostsFields { - public Map hosts; // host, fingerprint - } - - public static class ModpackContentFields { - public String modpackName = ""; - public String automodpackVersion = ""; - public String loader = ""; - public String loaderVersion = ""; - public String mcVersion = ""; - public Set list; - public Set nonModpackFilesToDelete = Set.of(); - - public ModpackContentFields(Set list) { - this.list = list; - } - - public ModpackContentFields() { - this.list = Set.of(); - } - public static class ModpackContentItem { - public final String file; - public final String size; - public final String type; - public final boolean editable; - public final boolean forceCopy; - public final String sha1; - public final String murmur; - - public ModpackContentItem(String file, String size, String type, boolean editable, boolean forceCopy, String sha1, String murmur) { - this.file = file; - this.size = size; - this.type = type; - this.editable = editable; - this.forceCopy = forceCopy; - this.sha1 = sha1; - this.murmur = murmur; - } - - @Override - public String toString() { - return String.format("ModpackContentItems(file=%s, size=%s, type=%s, editable=%s, forceCopy=%s, sha1=%s, murmur=%s)", file, size, type, editable, forceCopy, sha1, murmur); - } - - // if the relative file path is the same, we consider the items equal - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - ModpackContentItem that = (ModpackContentItem) obj; - return Objects.equals(file, that.file); - } - - @Override - public int hashCode() { - return Objects.hash(file); - } - } - - public static class FileToDelete { - public final String file; - public final String sha1; - public final String timestamp; - - public FileToDelete(String file, String sha1, String timestamp) { - this.file = file; - this.sha1 = sha1; - this.timestamp = timestamp; - } - } + public Map hosts; // host, fingerprint } // seems kinda too verbose and it may take too much space for large modpack but lets keep it for now public static class LocalMetadata { + // Map of File Path -> Fingerprint public Map files = new ConcurrentHashMap<>(); public static class FileFingerprint { + public final String sha1; - public final long lastSize; // Local disk size + public final long lastSize; // Local disk size public final long lastModified; // Local disk timestamp public FileFingerprint(String sha1, long lastSize, long lastModified) { @@ -235,7 +233,155 @@ public static class ClientDummyFiles { } public static class ClientDeletedNonModpackFilesTimestamps { + // Set of timestamps of the files to delete public Set timestamps = ConcurrentHashMap.newKeySet(); } + + public static class ModpackContent { + + public String modpackName = ""; + public String automodpackVersion; + public String loader; + public String loaderVersion; + public String mcVersion; + + public Map groups; + public Set nonModpackFilesToDelete; + + + public ModpackContent(Map groups) { + this.groups = groups; + this.nonModpackFilesToDelete = new HashSet<>(); + } + + public ModpackContent() { + this.groups = new HashMap<>(); + this.nonModpackFilesToDelete = new HashSet<>(); + } + } + + public static class ModpackGroupFields { + + // These values map directly from config - Jsons.GroupDeclaration + public String displayName; + public String description; + public String category; + public boolean required; + public boolean recommended; + public List breaksWith; + public List requires; + public List compatibleOS; + + // This part is dynamicly generated + public Set files = new HashSet<>(); + + public ModpackGroupFields() { + this.files = new HashSet<>(); + } + + public ModpackGroupFields(String displayName, String description, String category, boolean required, boolean recommended, List breaksWith, List requires, List compatibleOS) { + this.displayName = displayName; + this.description = description; + this.category = category; + this.required = required; + this.recommended = recommended; + this.breaksWith = breaksWith; + this.requires = requires; + this.compatibleOS = compatibleOS; + } + + public ModpackGroupFields(Jsons.GroupDeclaration groupDeclaration) { + this.displayName = groupDeclaration.displayName; + this.description = groupDeclaration.description; + this.category = groupDeclaration.category; + this.required = groupDeclaration.required; + this.recommended = groupDeclaration.recommended; + this.breaksWith = groupDeclaration.breaksWith; + this.requires = groupDeclaration.requires; + this.compatibleOS = groupDeclaration.compatibleOS; + } + } + + public static class ModpackContentItem { + + public final String file; + public final long size; + public final String type; + public final boolean editable; + public final boolean forceCopy; + public final String sha1; + public final String murmur; + + public ModpackContentItem(String file, long size, String type, boolean editable, boolean forceCopy, String sha1, String murmur) { + this.file = file; + this.size = size; + this.type = type; + this.editable = editable; + this.forceCopy = forceCopy; + this.sha1 = sha1; + this.murmur = murmur; + } + + @Override + public String toString() { + return String.format("ModpackContentItems(file=%s, size=%s, type=%s, editable=%s, forceCopy=%s, sha1=%s, murmur=%s)", file, size, type, editable, forceCopy, sha1, murmur); + } + + // if the relative file path is the same, we consider the items equal + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ModpackContentItem that = (ModpackContentItem) obj; + return Objects.equals(file, that.file); + } + + @Override + public int hashCode() { + return Objects.hash(file); + } + } + + public static class FileToDelete { + + public final String file; + public final String sha1; + public final String timestamp; + + public FileToDelete(String file, String sha1, String timestamp) { + this.file = file; + this.sha1 = sha1; + this.timestamp = timestamp; + } + } + + public static class ClientSelectionManagerFields { + public String selectedPack; + public Map modpacks = new HashMap<>(); + + public static class Modpack { + public ModpackAddresses modpackAddresses; + public List selectedGroups; + + public Modpack(ModpackAddresses modpackAddresses) { + this.modpackAddresses = modpackAddresses; + this.selectedGroups = new ArrayList<>(); + } + + public Modpack(ModpackAddresses modpackAddresses, List selectedGroups) { + this.modpackAddresses = modpackAddresses; + this.selectedGroups = selectedGroups; + } + } + + // TODO consider just a string field of groupId in the Modpack class instead + public static class Group { + public final String groupId; + + public Group(String groupId) { + this.groupId = groupId; + } + } + } } diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ClientSelectionManager.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ClientSelectionManager.java new file mode 100644 index 000000000..a17fe1e3d --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ClientSelectionManager.java @@ -0,0 +1,234 @@ +package pl.skidam.automodpack_core.modpack; + +import static pl.skidam.automodpack_core.Constants.LOGGER; +import static pl.skidam.automodpack_core.Constants.clientSelectionFile; + +import java.nio.file.Path; +import java.util.*; +import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.config.Jsons; + +import static pl.skidam.automodpack_core.config.Jsons.ClientSelectionManagerFields; + +/** + * Manages client-side modpack group selections and addresses. + * Handles saving/loading user preferences locally without requiring active server configuration. + */ +public class ClientSelectionManager { + + private final static ClientSelectionManager MANAGER = new ClientSelectionManager(clientSelectionFile); + + private final Path selectionFile; + private final ClientSelectionManagerFields selections; + + public static ClientSelectionManager getMgr() { + return MANAGER; + } + + private ClientSelectionManager(Path selectionFile) { + this.selectionFile = selectionFile; + this.selections = ConfigTools.loadClientSelectionManager(selectionFile); + + // Ensure the modpacks map is initialized if it loads as null from JSON + if (this.selections.modpacks == null) { + this.selections.modpacks = new HashMap<>(); + } + } + + /** + * Gets the ID of the currently selected modpack. + * * @return The selected modpack ID, or null if none is selected. + */ + public String getSelectedPackId() { + return selections.selectedPack; + } + + public Jsons.ModpackAddresses getSelectedAddresses() { + return getModpackAddresses(getSelectedPackId()); + } + + public List getSelectedGroups() { + return getSelectedGroups(getSelectedPackId()); + } + + // TODO make packids somehow unique and independet from modpackNames or server addresses + public boolean packExists(String packId) { + return selections.modpacks.containsKey(packId); + } + + public void removePack(String packId) { + selections.modpacks.remove(packId); + save(); + } + + public void addPack(String packId, ClientSelectionManagerFields.Modpack pack) { + if (selections.modpacks.containsKey(packId)) { + LOGGER.debug("Overwritting pack {}", packId); + } + selections.modpacks.put(packId, pack); + save(); + } + + /** + * Sets the currently active modpack ID. + * * @param packId The ID of the modpack to set as active. + */ + public void setSelectedPack(String packId) { + this.selections.selectedPack = packId; + save(); + } + + /** + * Internal helper to retrieve or initialize a modpack selection entry. + */ + private ClientSelectionManagerFields.Modpack getSelection(String packId) { + return selections.modpacks.get(packId); + } + + /** + * Gets the selected groups for a specific modpack. + * + * @param packId The modpack ID to query. + * @return A list of selected group IDs. + */ + public List getSelectedGroups(String packId) { + if (!selections.modpacks.containsKey(packId)) { + return new ArrayList<>(); + } + + List groups = selections.modpacks.get(packId).selectedGroups; + return groups == null ? new ArrayList<>() : new ArrayList<>(groups); + } + + /** + * Gets the selected groups for the currently active modpack. + * + * @return A list of selected group IDs for the active pack. + */ + public List getCurrentSelectedGroups() { + if (selections.selectedPack == null) { + return new ArrayList<>(); + } + return getSelectedGroups(selections.selectedPack); + } + + /** + * Dynamically returns the set of files chosen by the user for the specific modpack content. + * Falls back to default required/recommended combinations if no local selections exist. + * * @param content The server-provided Modpack Content manifest + * @return The precise set of selected files to synchronize + */ + public Set getSelectedFiles(Jsons.ModpackContent content) { + if (content == null || content.groups == null) { + return new HashSet<>(); + } + + List userSelectedGroups = getSelectedGroups(content.modpackName); + boolean hasSavedSelection = userSelectedGroups != null && !userSelectedGroups.isEmpty(); + + Set savedGroupFilesMap = new HashSet<>(); + if (hasSavedSelection) { + for (ClientSelectionManagerFields.Group g : userSelectedGroups) { + savedGroupFilesMap.add(g.groupId); + } + } + + Set finalFiles = new HashSet<>(); + + for (Map.Entry entry : content.groups.entrySet()) { + String id = entry.getKey(); + Jsons.ModpackGroupFields group = entry.getValue(); + + boolean shouldInclude; + if (hasSavedSelection) { + shouldInclude = savedGroupFilesMap.contains(id) || group.required; + } else { + shouldInclude = group.required || group.recommended; + } + + if (shouldInclude && group.files != null) { + finalFiles.addAll(group.files); + } + } + + return finalFiles; + } + + /** + * Sets the selected groups for a specific modpack. + * + * @param packId The modpack ID. + * @param selectedGroups The list of group IDs to save. + */ + public void setSelectedGroups(String packId, List selectedGroups) { + if (packId == null) { + LOGGER.warn("Attempted to set selected groups for a null pack ID."); + return; + } + + if (!packExists(packId)) { + LOGGER.warn("Attempted to set selected groups for a nonexistant pack"); + return; + } + + ClientSelectionManagerFields.Modpack selection = getSelection(packId); + selection.selectedGroups = new ArrayList<>(selectedGroups); + save(); + } + + /** + * Gets the addresses associated with a specific modpack. + * * @param packId The modpack ID to query. + * @return The modpack addresses, or null if not set. + */ + public Jsons.ModpackAddresses getModpackAddresses(String packId) { + if (!selections.modpacks.containsKey(packId)) { + return null; + } + return selections.modpacks.get(packId).modpackAddresses; + } + + /** + * Sets the addresses for a specific modpack. + * * @param packId The modpack ID. + * @param addresses The addresses to save. + */ + public void setModpackAddresses(String packId, Jsons.ModpackAddresses addresses) { + if (packId == null) { + LOGGER.warn("Attempted to set modpack addresses for a null pack ID."); + return; + } + + if (!packExists(packId)) { + LOGGER.warn("Attempted to set modpack addresses for a nonexistant pack"); + return; + } + + ClientSelectionManagerFields.Modpack selection = getSelection(packId); + selection.modpackAddresses = addresses; + save(); + } + + /** + * Clears all saved selections and addresses for a specific modpack. + * * @param packId The modpack ID to clear. + */ + public void clearSelection(String packId) { + if (packId != null) { + selections.modpacks.remove(packId); + + // If we are clearing the currently active pack, reset the active pointer + if (packId.equals(selections.selectedPack)) { + selections.selectedPack = null; + } + save(); + } + } + + /** + * Save the current selections to the filesystem. + */ + public void save() { + ConfigTools.saveClientSelectionManager(selectionFile, selections); + } +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/GroupContentScanner.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/GroupContentScanner.java new file mode 100644 index 000000000..5e10570bd --- /dev/null +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/GroupContentScanner.java @@ -0,0 +1,240 @@ +package pl.skidam.automodpack_core.modpack; + +import static pl.skidam.automodpack_core.Constants.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.loader.LoaderManagerService; +import pl.skidam.automodpack_core.utils.FileInspection; +import pl.skidam.automodpack_core.utils.FileTreeScanner; +import pl.skidam.automodpack_core.utils.HashUtils; +import pl.skidam.automodpack_core.utils.SmartFileUtils; +import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; + +/** + * Scans a single group directory based on its GroupDeclaration + * and generates ModpackContentItem entries. + */ +public class GroupContentScanner { + + private final String groupId; + private final Path groupDirectory; + private final FileTreeScanner syncedFilesScanner; + private final FileTreeScanner editableFilesScanner; + private final FileTreeScanner forceCopyScanner; + private final ThreadPoolExecutor executorService; + private final Map sha1MurmurMapCache; + private final FileMetadataCache cache; + + private final Jsons.ModpackGroupFields groupFields; + private final Map fileHashToPathMap = new ConcurrentHashMap<>(); + + // TODO consider getting murmur cache from actual db cache instead of the older modpack content + public GroupContentScanner(String groupId, Path groupDirectory, Jsons.GroupDeclaration groupDeclaration, + FileTreeScanner syncedFilesScanner, FileTreeScanner editableFilesScanner, + FileTreeScanner forceCopyScanner, ThreadPoolExecutor executorService, + Map sha1MurmurMapCache, FileMetadataCache cache) { + this.groupId = groupId; + this.groupDirectory = groupDirectory; + this.syncedFilesScanner = syncedFilesScanner; + this.editableFilesScanner = editableFilesScanner; + this.forceCopyScanner = forceCopyScanner; + this.executorService = executorService; + this.sha1MurmurMapCache = sha1MurmurMapCache; + this.cache = cache; + + this.groupFields = new Jsons.ModpackGroupFields(groupDeclaration); + } + + public void scanAndGenerate() { + Map finalFilesMap = new HashMap<>(); + + // Process syncedFiles (Lower Priority) + if (syncedFilesScanner != null) { + syncedFilesScanner.scan(); + for (Map.Entry entry : syncedFilesScanner.getMatchedPaths().entrySet()) { + String relativePath = SmartFileUtils.formatPath(entry.getValue(), SmartFileUtils.CWD); + finalFilesMap.put(relativePath, entry.getValue()); + } + } + + // Process host-modpack// directory (Higher Priority) + if (Files.exists(groupDirectory)) { + try (Stream paths = Files.walk(groupDirectory)) { + paths.filter(Files::isRegularFile).forEach(file -> { + String relativePath = SmartFileUtils.formatPath(file, groupDirectory); + finalFilesMap.put(relativePath, file); + }); + } catch (IOException e) { + LOGGER.error("Failed to walk group directory: " + groupDirectory, e); + } + } + + // Execute the secondary attribute scanners so their internal maps are populated! + if (editableFilesScanner != null) { + editableFilesScanner.scan(); + } + if (forceCopyScanner != null) { + forceCopyScanner.scan(); + } + + // Process hashes in parallel + List> futures = new ArrayList<>(); + + for (Map.Entry entry : finalFilesMap.entrySet()) { + String relativePath = entry.getKey(); + Path absoluteDiskPath = entry.getValue(); + + futures.add(CompletableFuture.supplyAsync(() -> processFile(absoluteDiskPath, relativePath), executorService)); + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + // Collect successful items + Set groupItems = new HashSet<>(); + for (CompletableFuture future : futures) { + try { + Jsons.ModpackContentItem item = future.get(); + if (item != null) { + groupItems.add(item); + } + } catch (Exception e) { + LOGGER.error("Failed to retrieve processed file item", e); + } + } + + this.groupFields.files = groupItems; + } + + private Jsons.ModpackContentItem processFile(Path absoluteDiskPath, String formattedFile) { + try { + long size = SmartFileUtils.size(absoluteDiskPath); + String type = determineFileType(absoluteDiskPath, formattedFile); + + if (type == null) { + return null; + } + + String sha1 = cache != null ? cache.getHashOrNull(absoluteDiskPath) : HashUtils.getHash(absoluteDiskPath); + + String murmur = null; + if ("mod".equals(type) || "shader".equals(type) || "resourcepack".equals(type)) { + murmur = sha1MurmurMapCache.get(sha1); + if (murmur == null) { + murmur = HashUtils.getCurseforgeMurmurHash(absoluteDiskPath); + if (murmur != null) { + sha1MurmurMapCache.put(sha1, murmur); + } + } + if ("mod".equals(type)) { + if (serverConfig.autoExcludeServerSideMods && Objects.equals(FileInspection.getModEnvironment(absoluteDiskPath), LoaderManagerService.EnvironmentType.SERVER)) { + LOGGER.info("File {} is server mod! Skipping...", formattedFile); + return null; + } + // Exclude AutoModpack itself + var modId = FileInspection.getModID(absoluteDiskPath); + if ((MOD_ID + "_bootstrap").equals(modId) || (MOD_ID + "-bootstrap").equals(modId) || (MOD_ID + "_mod").equals(modId) || MOD_ID.equals(modId)) { + return null; + } + } + } + + if (serverConfig.autoExcludeUnnecessaryFiles) { + if (size == 0) { + LOGGER.info("Skipping file {} because it is empty", formattedFile); + return null; + } + + if (absoluteDiskPath.getFileName().toString().startsWith(".")) { + LOGGER.info("Skipping file {} is hidden", formattedFile); + return null; + } + + // check if any parent dir name starts with a dot + if (StreamSupport.stream(absoluteDiskPath.getParent().spliterator(), false).anyMatch(p -> p.toString().startsWith("."))) { + LOGGER.info("Skipping file {} it is inside a hidden directory", formattedFile); + return null; + } + + if (formattedFile.endsWith(".tmp")) { + LOGGER.info("File {} is temporary! Skipping...", formattedFile); + return null; + } + + if (formattedFile.endsWith(".disabled")) { + LOGGER.info("File {} is disabled! Skipping...", formattedFile); + return null; + } + + if (formattedFile.endsWith(".bak")) { + LOGGER.info("File {} is backup file, unnecessary on client! Skipping...", formattedFile); + return null; + } + + if (formattedFile.equals("Zone.Identifier")) { + LOGGER.info("File {} is a Windows Zone.Identifier file, useless for client! Skipping...", formattedFile); + return null; + } + } + + boolean isEditable = editableFilesScanner != null && editableFilesScanner.hasMatch(formattedFile); + boolean forcedToCopy = forceCopyScanner != null && forceCopyScanner.hasMatch(formattedFile); + + fileHashToPathMap.put(sha1, absoluteDiskPath); + + return new Jsons.ModpackContentItem(formattedFile, size, type, isEditable, forcedToCopy, sha1, murmur); + + } catch (Exception e) { + LOGGER.error("Failed to process file: " + absoluteDiskPath, e); + return null; + } + } + + private String determineFileType(Path file, String formattedFile) { + if (FileInspection.isMod(file)) { + if (serverConfig != null && serverConfig.autoExcludeServerSideMods) { + var envType = FileInspection.getModEnvironment(file); + if (LoaderManagerService.EnvironmentType.SERVER.equals(envType)) { + LOGGER.debug("Skipping server-side mod {} in group {}", formattedFile, groupId); + return null; + } + } + return "mod"; + } else if (formattedFile.contains("/config/")) { + return "config"; + } else if (formattedFile.contains("/shaderpacks/")) { + return "shader"; + } else if (formattedFile.contains("/resourcepacks/")) { + return "resourcepack"; + } else if (formattedFile.endsWith("/options.txt")) { + return "mc_options"; + } else { + return "other"; + } + } + + public String getGroupId() { + return groupId; + } + + public Path getGroupDirectory() { + return groupDirectory; + } + + public Jsons.ModpackGroupFields getGroupFields() { + return groupFields; + } + + public Map getFileHashToPathMap() { + return fileHashToPathMap; + } +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java index bb9d67d86..77ddcc1b8 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackContent.java @@ -1,334 +1,38 @@ package pl.skidam.automodpack_core.modpack; -import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.loader.LoaderManagerService; -import pl.skidam.automodpack_core.utils.*; -import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; - -import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import static pl.skidam.automodpack_core.Constants.*; +import pl.skidam.automodpack_core.config.Jsons; public class ModpackContent { - public final Set list = ConcurrentHashMap.newKeySet(); - public final ObservableMap pathsMap = new ObservableMap<>(); - private final String MODPACK_NAME; - private final FileTreeScanner SYNCED_FILES_CARDS; - private final FileTreeScanner EDITABLE_CARDS; - private final FileTreeScanner FORCE_COPY_FILES_TO_STANDARD_LOCATION; - private final Path MODPACK_DIR; - private final ThreadPoolExecutor CREATION_EXECUTOR; - private final Map sha1MurmurMapPreviousContent = new HashMap<>(); - - public ModpackContent(String modpackName, Path cwd, Path modpackDir, Set syncedFiles, Set allowEditsInFiles, Set forceCopyFilesToStandardLocation, ThreadPoolExecutor CREATION_EXECUTOR) { - this.MODPACK_NAME = modpackName; - this.MODPACK_DIR = modpackDir; - this.CREATION_EXECUTOR = CREATION_EXECUTOR; - Set directoriesToSearch = new HashSet<>(2); - if (MODPACK_DIR != null) directoriesToSearch.add(MODPACK_DIR); - if (cwd != null) { - directoriesToSearch.add(cwd); - this.SYNCED_FILES_CARDS = new FileTreeScanner(syncedFiles, Set.of(cwd)); - } else { - this.SYNCED_FILES_CARDS = new FileTreeScanner(syncedFiles, Set.of()); - } - this.EDITABLE_CARDS = new FileTreeScanner(allowEditsInFiles, directoriesToSearch); - this.FORCE_COPY_FILES_TO_STANDARD_LOCATION = new FileTreeScanner(forceCopyFilesToStandardLocation, directoriesToSearch); - } - - public String getModpackName() { - return MODPACK_NAME; - } - - public boolean create(FileMetadataCache cache) { - Set computedFilesToDelete = new HashSet<>(); - - try { - SYNCED_FILES_CARDS.scan(); - EDITABLE_CARDS.scan(); - FORCE_COPY_FILES_TO_STANDARD_LOCATION.scan(); - - pathsMap.clear(); - sha1MurmurMapPreviousContent.clear(); - - getPreviousContent().ifPresent(previousContent -> { - Map oldFilesMap = previousContent.nonModpackFilesToDelete.stream() - .collect(Collectors.toMap(f -> f.file, f -> f, (a, b) -> a)); - - if (serverConfig != null && serverConfig.nonModpackFilesToDelete != null) { - for (var fileToDeleteEntry : serverConfig.nonModpackFilesToDelete.entrySet()) { - var file = fileToDeleteEntry.getKey(); - var sha1 = fileToDeleteEntry.getValue(); - if (oldFilesMap.containsKey(file) && oldFilesMap.get(file).sha1.equalsIgnoreCase(sha1)) { - computedFilesToDelete.add(oldFilesMap.get(file)); - } else { - String currentTimestamp = String.valueOf(System.currentTimeMillis()); - computedFilesToDelete.add(new Jsons.ModpackContentFields.FileToDelete(file, sha1, currentTimestamp)); - } - } - } - - previousContent.list.forEach(item -> sha1MurmurMapPreviousContent.put(item.sha1, item.murmur)); - }); - - Map filesToProcess = new HashMap<>(); - - SYNCED_FILES_CARDS.getMatchedPaths().values().forEach(path -> filesToProcess.put(SmartFileUtils.formatPath(path, MODPACK_DIR), path)); - - if (MODPACK_DIR != null) { - try (Stream stream = Files.walk(MODPACK_DIR)) { // in case there any files with the same relative path, we prefer from MODPACK_DIR, this will override previous entries - stream.forEach(path -> filesToProcess.put(SmartFileUtils.formatPath(path, MODPACK_DIR), path)); - } - } - - var tempPathMap = new HashMap<>(filesToProcess); - - List> futures = filesToProcess.entrySet().stream() - .map(entry -> CompletableFuture.supplyAsync(() -> { - try { - var contentEntry = generateContent(entry.getValue(), entry.getKey(), cache); - if (contentEntry == null) { - return null; - } - LOGGER.debug("Generated modpack content for {}", entry.getValue()); - tempPathMap.put(contentEntry.sha1, entry.getValue()); - return contentEntry; - } catch (Exception e) { - LOGGER.error("Error generating content for {}", entry.getValue(), e); - return null; - } - }, CREATION_EXECUTOR)) - .toList(); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - - for (var future : futures) { - Jsons.ModpackContentFields.ModpackContentItem item = future.join(); - if (item != null) { - list.add(item); - pathsMap.put(item.sha1, tempPathMap.get(item.sha1)); - } - } - - if (list.isEmpty()) { - LOGGER.warn("Modpack is empty!"); - return false; - } else { - LOGGER.info("Modpack generated with {} files!", list.size()); - } - } catch (Exception e) { - LOGGER.error("Error while generating modpack!", e); - return false; - } - - saveModpackContent(computedFilesToDelete); - if (hostServer != null) { - hostServer.setPaths(pathsMap); - } - - return true; - } - - public Optional getPreviousContent() { - var optionalModpackContentFile = ModpackContentTools.getModpackContentFile(MODPACK_DIR); - return optionalModpackContentFile.map(ConfigTools::loadModpackContent); - } - - public boolean loadPreviousContent() { - var optionalPreviousModpackContent = getPreviousContent(); - if (optionalPreviousModpackContent.isEmpty()) return false; - Jsons.ModpackContentFields previousModpackContent = optionalPreviousModpackContent.get(); - - synchronized (list) { - list.addAll(previousModpackContent.list); - - for (Jsons.ModpackContentFields.ModpackContentItem modpackContentItem : list) { - Path file = SmartFileUtils.getPath(MODPACK_DIR, modpackContentItem.file); - if (!Files.exists(file)) file = SmartFileUtils.getPathFromCWD(modpackContentItem.file); - if (!Files.exists(file)) { - LOGGER.warn("File {} does not exist!", file); - continue; - } - - pathsMap.put(modpackContentItem.sha1, file); - } - } - - if (hostServer != null) { - hostServer.setPaths(pathsMap); - } - saveModpackContent(previousModpackContent.nonModpackFilesToDelete); + private final Jsons.ModpackContent content; - return true; - } - - public synchronized void saveModpackContent(Set nonModpackFilesToDelete) { - if (nonModpackFilesToDelete == null) { - throw new IllegalArgumentException("filesToDelete is null"); - } - - synchronized (list) { - Jsons.ModpackContentFields modpackContent = new Jsons.ModpackContentFields(list); - - modpackContent.automodpackVersion = AM_VERSION; - modpackContent.mcVersion = MC_VERSION; - modpackContent.loaderVersion = LOADER_VERSION; - modpackContent.loader = LOADER; - modpackContent.modpackName = MODPACK_NAME; - modpackContent.nonModpackFilesToDelete = nonModpackFilesToDelete; + // A thread-safe map for caching file paths in memory for Netty server requests + private final Map pathsMap = new ConcurrentHashMap<>(); - ConfigTools.saveModpackContent(hostModpackContentFile, modpackContent); + public ModpackContent(Jsons.ModpackContent content, Map globalPathMap) { + this.content = content; + if (globalPathMap != null) { + this.pathsMap.putAll(globalPathMap); } } - public CompletableFuture replaceAsync(Path file, FileMetadataCache cache) { - return CompletableFuture.runAsync(() -> replace(file, cache), CREATION_EXECUTOR); + public String getModpackName() { + return content.modpackName; } - public void replace(Path file, FileMetadataCache cache) { - remove(file); - try { - String modpackFile = SmartFileUtils.formatPath(file, MODPACK_DIR); - Jsons.ModpackContentFields.ModpackContentItem item = generateContent(file, modpackFile, cache); - if (item != null) { - LOGGER.info("generated content for {}", item.file); - synchronized (list) { - list.add(item); - } - pathsMap.put(item.sha1, file); - } - } catch (Exception e) { - LOGGER.error("Error while replacing content for: " + file, e); - } + public Jsons.ModpackContent getContent() { + return content; } - public void remove(Path file) { - String modpackFile = SmartFileUtils.formatPath(file, MODPACK_DIR); - - synchronized (list) { - for (Jsons.ModpackContentFields.ModpackContentItem item : this.list) { - if (item.file.equals(modpackFile)) { - this.pathsMap.remove(item.sha1); - this.list.remove(item); - LOGGER.info("Removed content for {}", modpackFile); - break; - } - } - } + public boolean isEmpty() { + return pathsMap.isEmpty(); } - public static boolean isInnerFile(Path file) { - Path normalizedFilePath = file.toAbsolutePath().normalize(); - boolean isInner = normalizedFilePath.startsWith(automodpackDir.toAbsolutePath().normalize()) && !normalizedFilePath.startsWith(hostModpackDir.toAbsolutePath().normalize()); - if (!isInner && normalizedFilePath.equals(hostModpackContentFile.toAbsolutePath().normalize())) { // special case, since its inside hostModpackDir - return true; - } - - return isInner; - } - - private Jsons.ModpackContentFields.ModpackContentItem generateContent(final Path file, final String formattedFile, FileMetadataCache cache) throws Exception { - if (!Files.isRegularFile(file)) return null; - - if (serverConfig == null) { - LOGGER.error("Server config is null!"); - return null; - } - - if (isInnerFile(file)) { - return null; - } - - if (formattedFile.startsWith("/automodpack/")) { - return null; - } - - final String size = String.valueOf(Files.size(file)); - - if (serverConfig.autoExcludeUnnecessaryFiles) { - if (size.equals("0")) { - LOGGER.info("Skipping file {} because it is empty", formattedFile); - return null; - } - - if (file.getFileName().toString().startsWith(".")) { - LOGGER.info("Skipping file {} is hidden", formattedFile); - return null; - } - - if (formattedFile.endsWith(".tmp")) { - LOGGER.info("File {} is temporary! Skipping...", formattedFile); - return null; - } - - if (formattedFile.endsWith(".disabled")) { - LOGGER.info("File {} is disabled! Skipping...", formattedFile); - return null; - } - - if (formattedFile.endsWith(".bak")) { - LOGGER.info("File {} is backup file, unnecessary on client! Skipping...", formattedFile); - return null; - } - } - - String type; - - if (FileInspection.isMod(file)) { - type = "mod"; - if (serverConfig.autoExcludeServerSideMods && Objects.equals(FileInspection.getModEnvironment(file), LoaderManagerService.EnvironmentType.SERVER)) { - LOGGER.info("File {} is server mod! Skipping...", formattedFile); - return null; - } - // Exclude AutoModpack itself - var modId = FileInspection.getModID(file); - if ((MOD_ID + "_bootstrap").equals(modId) || (MOD_ID + "-bootstrap").equals(modId) || (MOD_ID + "_mod").equals(modId) || MOD_ID.equals(modId)) { - return null; - } - } else if (formattedFile.contains("/config/")) { - type = "config"; - } else if (formattedFile.contains("/shaderpacks/")) { - type = "shader"; - } else if (formattedFile.contains("/resourcepacks/")) { - type = "resourcepack"; - } else if (formattedFile.endsWith("/options.txt")) { - type = "mc_options"; - } else { - type = "other"; - } - - String sha1 = cache != null ? cache.getHashOrNull(file) : HashUtils.getHash(file); - - // For CF API - String murmur = null; - if (type.equals("mod") || type.equals("shader") || type.equals("resourcepack")) { - murmur = sha1MurmurMapPreviousContent.get(sha1); // Get from cache - if (murmur == null) { - murmur = HashUtils.getCurseforgeMurmurHash(file); - } - } - - boolean isEditable = false; - if (EDITABLE_CARDS.hasMatch(formattedFile)) { - isEditable = true; - LOGGER.info("File {} is editable!", formattedFile); - } - - boolean forcedToCopy = false; - if (FORCE_COPY_FILES_TO_STANDARD_LOCATION.hasMatch(formattedFile)) { - forcedToCopy = true; - LOGGER.info("File {} is forced to copy to standard location!", formattedFile); - } - - return new Jsons.ModpackContentFields.ModpackContentItem(formattedFile, size, type, isEditable, forcedToCopy, sha1, murmur); + public Path getPath(String hash) { + return pathsMap.get(hash); } } \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackExecutor.java b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackExecutor.java index 3535c9142..efa0bb2d7 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackExecutor.java +++ b/core/src/main/java/pl/skidam/automodpack_core/modpack/ModpackExecutor.java @@ -1,74 +1,189 @@ package pl.skidam.automodpack_core.modpack; -import pl.skidam.automodpack_core.utils.*; -import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; +import static pl.skidam.automodpack_core.Constants.*; -import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; -import static pl.skidam.automodpack_core.Constants.*; +import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; +import pl.skidam.automodpack_core.utils.FileTreeScanner; +import pl.skidam.automodpack_core.utils.SmartFileUtils; +import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; +/** + * Group-based Modpack Executor. + * Orchestrates the scanning and generation of modpack content based on server groups. + */ public class ModpackExecutor { - private final ThreadPoolExecutor CREATION_EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() * 2), new CustomThreadFactoryBuilder().setNameFormat("AutoModpackCreation-%d").build()); + + private final ThreadPoolExecutor CREATION_EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool( + Math.max(1, Runtime.getRuntime().availableProcessors() * 2), + new CustomThreadFactoryBuilder().setNameFormat("AutoModpackCreation-%d").build() + ); + public final Map modpacks = new ConcurrentHashMap<>(); - private ModpackContent init() { - if (isGenerating()) { - LOGGER.error("Called init() while generating!"); - return null; - } + // Use ConcurrentHashMap for thread safety across scanners + private final Map sha1MurmurMapCache = new ConcurrentHashMap<>(); + private final Map groupScanners = new ConcurrentHashMap<>(); - try { - if (!Files.exists(hostContentModpackDir)) { - Files.createDirectories(hostContentModpackDir); - Files.createDirectory(hostContentModpackDir.resolve("mods")); - Files.createDirectory(hostContentModpackDir.resolve("config")); - Files.createDirectory(hostContentModpackDir.resolve("shaderpacks")); - Files.createDirectory(hostContentModpackDir.resolve("resourcepacks")); - } - } catch (IOException e) { - LOGGER.error("Failed to create modpack content directory!", e); - return null; - } + // Precise state tracking + private final AtomicBoolean isGenerating = new AtomicBoolean(false); - return new ModpackContent(serverConfig.modpackName, SmartFileUtils.CWD, hostContentModpackDir, serverConfig.syncedFiles, serverConfig.allowEditsInFiles, serverConfig.forceCopyFilesToStandardLocation, CREATION_EXECUTOR); + public ModpackExecutor() { + // Initialization } - public boolean generateNew(ModpackContent content) { - if (content == null) return false; - boolean generated; - try (var cache = FileMetadataCache.open(hashCacheDBFile)) { - generated = content.create(cache); + /** + * Called by ServerMessageHandler to refresh specific files. + * The FileMetadataCache will automatically skip unmodified files, making this very performant. + */ + // TODO consider actually refreshing only the files client asked for instead of full regeneration since it may add new files which we didnt want? + public CompletableFuture refreshFiles(Set hashes) { + if (hashes == null || hashes.isEmpty()) { + return CompletableFuture.completedFuture(null); } - modpacks.put(content.getModpackName(), content); - return generated; + + LOGGER.info("Requested refresh for {} files. Triggering modpack regeneration...", hashes.size()); + + // Run asynchronously so we don't block the Netty worker thread + return CompletableFuture.runAsync(this::generateNew, CREATION_EXECUTOR).exceptionally(ex -> { + LOGGER.error("Failed to refresh files during regeneration", ex); + return null; + }); } + // TODO use Semaphores instead of atomic integers so we can stale requests and imediatly say that they are done whenever whichever thread finishes first public boolean generateNew() { - ModpackContent content = init(); - if (content == null) return false; - boolean generated; - try (var cache = FileMetadataCache.open(hashCacheDBFile)) { - generated = content.create(cache); + if (!isGenerating.compareAndSet(false, true)) { + LOGGER.warn("Called generateNew() while already generating!"); + return false; + } + + try { + if (!Files.exists(hostModpackDir)) { + Files.createDirectories(hostModpackDir); + } + + try (var cache = FileMetadataCache.open(hashCacheDBFile)) { + + Jsons.ModpackContent content = new Jsons.ModpackContent(); + content.modpackName = serverConfig.modpackName; + + Map globalPathMap = new ConcurrentHashMap<>(); + groupScanners.clear(); // Clear existing scanners before regenerating + + // Get defined groups from server configuration + Map declaredGroups = serverConfig.groups; + LOGGER.info("Groups to include: {}", declaredGroups.keySet()); + + // Execute scanners dynamically based on the group definitions mapped in config + for (Map.Entry groupEntry : declaredGroups.entrySet()) { + String groupId = groupEntry.getKey(); + Jsons.GroupDeclaration decl = groupEntry.getValue(); + + Path groupDir = hostModpackDir.resolve(groupId); + if (!Files.exists(groupDir)) { + Files.createDirectories(groupDir); + Files.createDirectory(groupDir.resolve("mods")); + Files.createDirectory(groupDir.resolve("config")); + Files.createDirectory(groupDir.resolve("shaderpacks")); + Files.createDirectory(groupDir.resolve("resourcepacks")); + } + + // Create specific scanners for this exact group + FileTreeScanner syncedFilesScanner = new FileTreeScanner(new HashSet<>(decl.syncedFiles), Set.of(SmartFileUtils.CWD)); + FileTreeScanner editableFilesScanner = new FileTreeScanner(new HashSet<>(decl.allowEditsInFiles), Set.of(SmartFileUtils.CWD, groupDir)); + FileTreeScanner forceCopyScanner = new FileTreeScanner(new HashSet<>(decl.forceCopyFilesToStandardLocation), Set.of(SmartFileUtils.CWD, groupDir)); + + GroupContentScanner scanner = new GroupContentScanner( + groupId, groupDir, decl, syncedFilesScanner, editableFilesScanner, forceCopyScanner, + CREATION_EXECUTOR, sha1MurmurMapCache, cache + ); + + groupScanners.put(groupId, scanner); + scanner.scanAndGenerate(); + + content.groups.put(groupId, scanner.getGroupFields()); + globalPathMap.putAll(scanner.getFileHashToPathMap()); + } + + ModpackContent finalContent = new ModpackContent(content, globalPathMap); + this.modpacks.put(content.modpackName, finalContent); + + ConfigTools.saveModpackContent(hostModpackContentFile, content); + return true; + } + } catch (Exception e) { + LOGGER.error("Error generating modpack", e); + return false; + } finally { + isGenerating.set(false); } - modpacks.put(content.getModpackName(), content); - return generated; } public boolean loadLast() { - ModpackContent content = init(); - if (content == null) return false; - boolean generated = content.loadPreviousContent(); - modpacks.put(content.getModpackName(), content); - return generated; + if (!isGenerating.compareAndSet(false, true)) { + LOGGER.warn("Called loadLast() while generating!"); + return false; + } + + try { + if (!Files.exists(hostModpackContentFile)) { + return false; + } + + Jsons.ModpackContent jsonContent = ConfigTools.loadModpackContent(hostModpackContentFile); + if (jsonContent == null || jsonContent.groups == null) { + return false; + } + + Map globalPathMap = new ConcurrentHashMap<>(); + + for (Map.Entry entry : jsonContent.groups.entrySet()) { + String groupId = entry.getKey(); + Jsons.ModpackGroupFields groupFields = entry.getValue(); + Path groupDir = hostModpackDir.resolve(groupId); + + if (groupFields.files == null) continue; + + for (Jsons.ModpackContentItem item : groupFields.files) { + if (item.sha1 != null && !item.sha1.isEmpty()) { + Path groupPath = SmartFileUtils.getPath(groupDir, item.file); + Path serverPath = SmartFileUtils.getPathFromCWD(item.file); + + if (Files.exists(groupPath)) { + globalPathMap.put(item.sha1, groupPath); + } else if (Files.exists(serverPath)) { + globalPathMap.put(item.sha1, serverPath); + } else { + LOGGER.warn("File listed in modpack content but missing from disk: {}", item.file); + } + } + } + } + + ModpackContent content = new ModpackContent(jsonContent, globalPathMap); + modpacks.put(jsonContent.modpackName, content); + + LOGGER.info("Modpack '{}' loaded successfully.", jsonContent.modpackName); + return true; + + } catch (Exception e) { + LOGGER.error("Error loading last modpack", e); + return false; + } finally { + isGenerating.set(false); + } } public boolean isGenerating() { - int activeCount = CREATION_EXECUTOR.getActiveCount(); - int queueSize = CREATION_EXECUTOR.getQueue().size(); - return activeCount > 0 || queueSize > 0; + return isGenerating.get(); } public ThreadPoolExecutor getExecutor() { @@ -77,5 +192,12 @@ public ThreadPoolExecutor getExecutor() { public void stop() { CREATION_EXECUTOR.shutdown(); + try { + if (!CREATION_EXECUTOR.awaitTermination(5, TimeUnit.SECONDS)) { + CREATION_EXECUTOR.shutdownNow(); + } + } catch (InterruptedException e) { + CREATION_EXECUTOR.shutdownNow(); + } } } \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java index a3aea68aa..590d1f5f7 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/NetUtils.java @@ -1,14 +1,5 @@ package pl.skidam.automodpack_core.protocol; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import pl.skidam.automodpack_core.utils.SmartFileUtils; -import pl.skidam.automodpack_core.utils.LockFreeInputStream; - import java.io.InputStream; import java.math.BigInteger; import java.nio.file.Files; @@ -22,6 +13,14 @@ import java.util.Calendar; import java.util.Date; import java.util.HexFormat; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import pl.skidam.automodpack_core.utils.LockFreeInputStream; +import pl.skidam.automodpack_core.utils.SmartFileUtils; public class NetUtils { @@ -50,6 +49,13 @@ public class NetUtils { public static final byte CONFIGURATION_ECHO_TYPE = 0x40; public static final byte CONFIGURATION_COMPRESSION_TYPE = 0x41; public static final byte CONFIGURATION_CHUNK_SIZE_TYPE = 0x42; + public static final byte CONFIGURATION_GROUP_TYPE = 0x43; + + // V3 Request message types + public static final byte GROUP_SELECTION_TYPE = 0x10; + + // Compression + public static final int COMPRESSION_THRESHOLD = 65536; // 64KB // Chunk size public static final int DEFAULT_CHUNK_SIZE = 256 * 1024; // 256 KB @@ -97,9 +103,7 @@ public static X509Certificate selfSign(KeyPair keyPair) throws Exception { } public static void saveCertificate(X509Certificate cert, Path path) throws Exception { - String certPem = "-----BEGIN CERTIFICATE-----\n" - + formatBase64(Base64.getEncoder().encodeToString(cert.getEncoded())) - + "-----END CERTIFICATE-----"; + String certPem = "-----BEGIN CERTIFICATE-----\n" + formatBase64(Base64.getEncoder().encodeToString(cert.getEncoded())) + "-----END CERTIFICATE-----"; SmartFileUtils.createParentDirs(path); Files.writeString(path, certPem); } @@ -114,9 +118,7 @@ public static X509Certificate loadCertificate(Path path) throws Exception { public static void savePrivateKey(PrivateKey key, Path path) throws Exception { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key.getEncoded()); - String keyPem = "-----BEGIN PRIVATE KEY-----\n" - + formatBase64(Base64.getEncoder().encodeToString(keySpec.getEncoded())) - + "-----END PRIVATE KEY-----"; + String keyPem = "-----BEGIN PRIVATE KEY-----\n" + formatBase64(Base64.getEncoder().encodeToString(keySpec.getEncoded())) + "-----END PRIVATE KEY-----"; SmartFileUtils.createParentDirs(path); Files.writeString(path, keyPem); } diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java index e022cf9e2..ed59336c4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/NettyServer.java @@ -22,13 +22,12 @@ import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.ConcurrentHashMap; - import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.modpack.ModpackContent; import pl.skidam.automodpack_core.protocol.NetUtils; import pl.skidam.automodpack_core.protocol.netty.handler.ProtocolServerHandler; import pl.skidam.automodpack_core.utils.AddressHelpers; import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder; -import pl.skidam.automodpack_core.utils.ObservableMap; public class NettyServer { @@ -36,8 +35,10 @@ public class NettyServer { public static final AttributeKey COMPRESSION_TYPE = AttributeKey.valueOf("COMPRESSION_TYPE"); public static final AttributeKey CHUNK_SIZE = AttributeKey.valueOf("CHUNK_SIZE"); public static final AttributeKey PROTOCOL_VERSION = AttributeKey.valueOf("PROTOCOL_VERSION"); + public static final AttributeKey GROUP_MESSAGE = AttributeKey.valueOf("GROUP_MESSAGE"); + private final Map connections = new ConcurrentHashMap<>(); - private final Map paths = new ConcurrentHashMap<>(); + private MultithreadEventLoopGroup eventLoopGroup; private ChannelFuture serverChannel; private Boolean shouldHost = false; // needed for stop modpack hosting for minecraft port @@ -64,18 +65,16 @@ public String getCertificateFingerprint() { return certificateFingerprint; } - public void setPaths(ObservableMap paths) { - this.paths.putAll(paths.getMap()); - paths.addOnPutCallback(this.paths::put); - paths.addOnRemoveCallback(this.paths::remove); - } - - public void removePaths(ObservableMap paths) { - paths.getMap().forEach(this.paths::remove); - } - public Optional getPath(String hash) { - return Optional.ofNullable(paths.get(hash)); + if (modpackExecutor != null) { + for (ModpackContent content : modpackExecutor.modpacks.values()) { + Path path = content.getPath(hash); + if (path != null) { + return Optional.of(path); + } + } + } + return Optional.empty(); } public Optional start() { @@ -110,14 +109,11 @@ public Optional start() { // Shiny TLS 1.3 sslCtx = SslContextBuilder.forServer(serverCertFile.toFile(), serverPrivateKeyFile.toFile()) - .sslProvider(SslProvider.JDK) - .protocols("TLSv1.3") - .ciphers(Arrays.asList( - "TLS_AES_128_GCM_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256")) - .sessionTimeout(1800) - .build(); + .sslProvider(SslProvider.JDK) + .protocols("TLSv1.3") + .ciphers(Arrays.asList("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256")) + .sessionTimeout(1800) + .build(); // generate sha256 from cert as a fingerprint certificateFingerprint = NetUtils.getFingerprint(cert); @@ -156,18 +152,20 @@ public Optional start() { new TrafficShaper(eventLoopGroup); serverChannel = new ServerBootstrap() - .channel(socketChannelClass) - .childOption(ChannelOption.TCP_NODELAY, true) - .childHandler(new ChannelInitializer() { + .channel(socketChannelClass) + .childOption(ChannelOption.TCP_NODELAY, true) + .childHandler( + new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(MOD_ID, new ProtocolServerHandler(sslCtx)); } - }) - .group(eventLoopGroup) - .localAddress(bindAddress) - .bind() - .syncUninterruptibly(); + } + ) + .group(eventLoopGroup) + .localAddress(bindAddress) + .bind() + .syncUninterruptibly(); } catch (Exception e) { LOGGER.error("Failed to start Netty server", e); return Optional.empty(); @@ -220,7 +218,17 @@ private boolean canStart() { return false; } - if (paths.isEmpty()) { + boolean hasFiles = false; + if (modpackExecutor != null) { + for (ModpackContent content : modpackExecutor.modpacks.values()) { + if (!content.isEmpty()) { + hasFiles = true; + break; + } + } + } + + if (!hasFiles) { LOGGER.warn("No file to host. Can't start modpack host server."); return false; } @@ -250,4 +258,4 @@ private boolean canStart() { return true; // Start separate server for modpack hosting } } -} +} \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java index 64c3ffe6c..53be96d39 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/handler/ServerMessageHandler.java @@ -15,17 +15,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.CompletableFuture; import pl.skidam.automodpack_core.auth.Secrets; -import pl.skidam.automodpack_core.config.Jsons; -import pl.skidam.automodpack_core.modpack.ModpackContent; import pl.skidam.automodpack_core.protocol.netty.NettyServer; import pl.skidam.automodpack_core.protocol.netty.message.ProtocolMessage; import pl.skidam.automodpack_core.protocol.netty.message.request.EchoMessage; import pl.skidam.automodpack_core.protocol.netty.message.request.FileRequestMessage; import pl.skidam.automodpack_core.protocol.netty.message.request.RefreshRequestMessage; import pl.skidam.automodpack_core.utils.LockFreeInputStream; -import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; +import pl.skidam.automodpack_core.utils.SmartFileUtils; public class ServerMessageHandler extends SimpleChannelInboundHandler { @@ -85,52 +82,22 @@ protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) thro private void refreshModpackFiles(ChannelHandlerContext context, byte[][] FileHashesList) throws IOException { Set hashes = new HashSet<>(); for (byte[] hash : FileHashesList) { - hashes.add(new String(hash)); - } - LOGGER.info("Received refresh request for files of hashes: {}", hashes); - List> creationFutures = new ArrayList<>(); - Set modpacks = new HashSet<>(); - - try (var cache = FileMetadataCache.open(hashCacheDBFile)) { - for (String hash : hashes) { - final Optional optionalPath = resolvePath(hash); - if (optionalPath.isEmpty()) continue; - Path path = optionalPath.get(); - ModpackContent modpack = null; - - for (var content : modpackExecutor.modpacks.values()) { - if (!content.pathsMap.getMap().containsKey(hash)) { - continue; - } - - modpack = content; - break; - } - - if (modpack == null) { - continue; - } - - modpacks.add(modpack); - creationFutures.add(modpack.replaceAsync(path, cache)); - } + hashes.add(new String(hash, CharsetUtil.UTF_8)); } - creationFutures.forEach(CompletableFuture::join); - modpacks.forEach(modpackContent -> { - var optionalPreviousModpackContent = modpackContent.getPreviousContent(); - if (optionalPreviousModpackContent.isEmpty()) { // How? - LOGGER.error("Could not find previous modpack content for modpack while refreshing it: {}", modpackContent.getModpackName()); - return; - } - Jsons.ModpackContentFields previousModpackContent = optionalPreviousModpackContent.get(); - modpackContent.saveModpackContent(previousModpackContent.nonModpackFilesToDelete); - }); - - LOGGER.info("Sending new modpack-content.json"); + LOGGER.info("Received refresh request for files of hashes: {}", hashes); - // Sends new json - sendFile(context, new byte[0]); + modpackExecutor.refreshFiles(hashes) + .thenRun(() -> { + LOGGER.info("Refreshed files, sending updated modpack-content.json"); + // Send new json + sendFile(context, new byte[0]); + }) + .exceptionally(ex -> { + LOGGER.error("Error refreshing files", ex); + sendError(context, protocolVersion, "Internal server error during refresh"); + return null; + }); } private boolean validateSecret(ChannelHandlerContext ctx, SocketAddress address, byte[] secret) { @@ -151,7 +118,7 @@ private boolean validateSecret(ChannelHandlerContext ctx, SocketAddress address, return valid; } - private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOException { + private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) { final String sha1 = new String(bsha1, CharsetUtil.UTF_8); final Optional optionalPath = resolvePath(sha1); @@ -161,7 +128,7 @@ private void sendFile(ChannelHandlerContext ctx, byte[] bsha1) throws IOExceptio } final Path path = optionalPath.get(); - final long fileSize = Files.size(path); + final long fileSize = SmartFileUtils.size(path); ByteBuf responseHeader = ctx.alloc().buffer(1 + 1 + 8); responseHeader.writeByte(this.protocolVersion); diff --git a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ConfigurationMessage.java b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ConfigurationMessage.java index 054b201d2..156bbd931 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ConfigurationMessage.java +++ b/core/src/main/java/pl/skidam/automodpack_core/protocol/netty/message/ConfigurationMessage.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; public abstract class ConfigurationMessage { + private final byte version; // 1 byte private final byte type; // 1 byte diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/ModpackContentTools.java b/core/src/main/java/pl/skidam/automodpack_core/utils/ModpackContentTools.java index 388ee1163..dbd0903da 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/ModpackContentTools.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/ModpackContentTools.java @@ -1,22 +1,13 @@ package pl.skidam.automodpack_core.utils; -import pl.skidam.automodpack_core.config.Jsons; +import static pl.skidam.automodpack_core.Constants.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; - -import static pl.skidam.automodpack_core.Constants.*; +import pl.skidam.automodpack_core.config.Jsons; public class ModpackContentTools { - public static String getFileType(String file, Jsons.ModpackContentFields list) { - for (Jsons.ModpackContentFields.ModpackContentItem item : list.list) { - if (item.file.contains(file)) { // compare file absolute path if it contains item.file - return item.type; - } - } - return "other"; - } public static Optional getModpackDir(String modpack) { if (modpack == null || modpack.isEmpty()) { diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/ObservableMap.java b/core/src/main/java/pl/skidam/automodpack_core/utils/ObservableMap.java deleted file mode 100644 index e662476a6..000000000 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/ObservableMap.java +++ /dev/null @@ -1,59 +0,0 @@ -package pl.skidam.automodpack_core.utils; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; - -@SuppressWarnings("unchecked") -public class ObservableMap { - - private final ConcurrentHashMap synchronizedMap; - private List> onPutCallbacks = new ArrayList<>(); - private List> onRemoveCallbacks = new ArrayList<>() ; - - public ObservableMap() { - synchronizedMap = new ConcurrentHashMap<>(); - } - - public ObservableMap(int initialCapacity) { - synchronizedMap = new ConcurrentHashMap<>(initialCapacity); - } - - public ObservableMap(Map m) { - synchronizedMap = new ConcurrentHashMap<>(m); - } - - public synchronized V put(K key, V value) { - V result = synchronizedMap.put(key, value); - for (BiConsumer callback : onPutCallbacks) { - callback.accept(key, value); - } - return result; - } - - public synchronized V remove(Object key) { - V result = synchronizedMap.remove((K) key); - for (BiConsumer callback : onRemoveCallbacks) { - callback.accept((K) key, result); - } - return result; - } - - public void clear() { - synchronizedMap.clear(); - this.onPutCallbacks = new ArrayList<>(); - this.onRemoveCallbacks = new ArrayList<>(); - } - - public void addOnPutCallback(BiConsumer callback) { - onPutCallbacks.add(callback); - } - - public void addOnRemoveCallback(BiConsumer callback) { - onRemoveCallbacks.add(callback); - } - - public Map getMap() { - return Collections.unmodifiableMap(synchronizedMap); - } -} diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/PlatformUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/PlatformUtils.java index 010ff929a..3dfef9eb4 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/PlatformUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/PlatformUtils.java @@ -1,20 +1,74 @@ package pl.skidam.automodpack_core.utils; -import pl.skidam.automodpack_core.protocol.compression.CompressionCodec; -import pl.skidam.automodpack_core.protocol.compression.CompressionFactory; -import java.util.Locale; import static pl.skidam.automodpack_core.Constants.LOGGER; import static pl.skidam.automodpack_core.protocol.NetUtils.COMPRESSION_ZSTD; +import java.util.Locale; +import pl.skidam.automodpack_core.protocol.compression.CompressionCodec; +import pl.skidam.automodpack_core.protocol.compression.CompressionFactory; + +/** + * Utility class for platform/OS detection + */ + public class PlatformUtils { + public enum OS { + WINDOWS, + LINUX, + MACOS, + UNKNOWN, + } + public static final boolean IS_MAC; public static final boolean IS_WIN; + public static final boolean IS_LINUX; + private static final OS DETECTED_OS; static { - String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); - IS_MAC = os.contains("mac"); - IS_WIN = os.contains("win"); + String osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); + IS_MAC = osName.contains("mac"); + IS_WIN = osName.contains("win"); + IS_LINUX = osName.contains("linux"); + + if (IS_WIN) { + DETECTED_OS = OS.WINDOWS; + } else if (IS_MAC) { + DETECTED_OS = OS.MACOS; + } else if (IS_LINUX) { + DETECTED_OS = OS.LINUX; + } else { + DETECTED_OS = OS.UNKNOWN; + } + } + + /** + * Get the current operating system + * + * @return the detected OS + */ + public static OS getCurrentOS() { + return DETECTED_OS; + } + + /** + * Check if the current OS matches the given OS name + * + * @param osName the OS name to check (case-insensitive, e.g., "windows", + * "linux", "macos") + * @return true if the current OS matches + */ + public static boolean isCurrentOS(String osName) { + if (osName == null || osName.isBlank()) { + return true; // Empty or null means compatible with all + } + try { + OS targetOS = OS.valueOf(osName.toUpperCase()); + return DETECTED_OS == targetOS; + } catch (IllegalArgumentException e) { + LOGGER.warn("Unknown OS name: {}", osName); + return false; + } } // Lazy load @@ -22,10 +76,10 @@ public class PlatformUtils { public static boolean canUseZstd() { if (zstd != null) return zstd; - + synchronized (PlatformUtils.class) { - if (zstd != null) return zstd; - try { + if (zstd != null) return zstd; + try { CompressionCodec compressionCodec = CompressionFactory.getCodec(COMPRESSION_ZSTD); zstd = compressionCodec.isInitialized(); } catch (Throwable e) { @@ -35,4 +89,4 @@ public static boolean canUseZstd() { return zstd; } } -} \ No newline at end of file +} diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/SmartFileUtils.java b/core/src/main/java/pl/skidam/automodpack_core/utils/SmartFileUtils.java index cb3250cbe..d3def35c5 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/SmartFileUtils.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/SmartFileUtils.java @@ -167,4 +167,13 @@ public static String formatPath(final Path modpackFile, final Path modpackPath) formattedFile = formattedFile.replace(File.separator, "/"); return formattedFile.startsWith("/") ? formattedFile : "/" + formattedFile; } + + // Use only if you checked file existance + public static long size(Path path) { + try { + return Files.size(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } \ No newline at end of file diff --git a/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java b/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java index ccb3d18fa..3f30c5f73 100644 --- a/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java +++ b/core/src/main/java/pl/skidam/automodpack_core/utils/WorkaroundUtil.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.Set; @@ -20,7 +21,7 @@ public WorkaroundUtil(Path modapckPath) { // returns list of formatted modpack files which are mods with services (these mods need special treatment in order to work properly) // mods returned by this method should be installed in standard `~/mods/` directory - public Set getWorkaroundMods(Jsons.ModpackContentFields modpackContentFields) throws IOException { + public Set getWorkaroundMods(Jsons.ModpackContent ModpackContent) throws IOException { Set workaroundMods = new HashSet<>(); // this workaround is needed only for neo/forge mods @@ -28,12 +29,16 @@ public Set getWorkaroundMods(Jsons.ModpackContentFields modpackContentFi return workaroundMods; } - for (Jsons.ModpackContentFields.ModpackContentItem item : modpackContentFields.list) { - if (item.type.equals("mod")) { - Path modPath = SmartFileUtils.getPath(modpackPath, item.file); - try (FileSystem fs = FileSystems.newFileSystem(modPath)) { - if (FileInspection.hasSpecificServices(fs)) { - workaroundMods.add(item.file); + for (var entry : ModpackContent.groups.entrySet()) { + Jsons.ModpackGroupFields group = entry.getValue(); + for (Jsons.ModpackContentItem item : group.files) { // TODO loop though the installed groups + if (item.type.equals("mod")) { + Path modPath = SmartFileUtils.getPath(modpackPath, item.file); + if (!Files.exists(modPath)) continue; + try (FileSystem fs = FileSystems.newFileSystem(modPath)) { + if (FileInspection.hasSpecificServices(fs)) { + workaroundMods.add(item.file); + } } } } diff --git a/core/src/test/java/pl/skidam/automodpack_core/modpack/ModpackTest.java b/core/src/test/java/pl/skidam/automodpack_core/modpack/ModpackTest.java index 1ee4cfa5b..b02adf0b1 100644 --- a/core/src/test/java/pl/skidam/automodpack_core/modpack/ModpackTest.java +++ b/core/src/test/java/pl/skidam/automodpack_core/modpack/ModpackTest.java @@ -5,12 +5,17 @@ import org.junit.jupiter.api.io.TempDir; import pl.skidam.automodpack_core.Constants; import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.utils.FileTreeScanner; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; import static org.junit.jupiter.api.Assertions.*; import static pl.skidam.automodpack_core.Constants.DEBUG; @@ -96,22 +101,43 @@ void modpackTest() { "ModpackContentItems(file=/mods/server-mod-1.20.jar, size=1, type=other, editable=false, forceCopy=false, sha1=86f7e437faa5a7fce15d1ddcb9eaeaea377667b8, murmur=null)" ); - Constants.serverConfig = new Jsons.ServerConfigFieldsV2(); + Constants.serverConfig = new Jsons.ServerConfigFieldsV3(); Constants.serverConfig.autoExcludeUnnecessaryFiles = false; + Constants.serverConfig.autoExcludeServerSideMods = false; + + // Configure dummy group definition + Jsons.GroupDeclaration decl = new Jsons.GroupDeclaration(); + decl.allowEditsInFiles = editable; + + // FIXED: Give the scanner the test directory so it actually has a location to scan from! + FileTreeScanner editableScanner = new FileTreeScanner(new HashSet<>(editable), Set.of(testFilesDir)); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); + + // Run the scanner purely in-memory bypassing file saving + GroupContentScanner scanner = new GroupContentScanner( + "main", + testFilesDir, + decl, + null, + editableScanner, + null, + executor, + new ConcurrentHashMap<>(), + null + ); - ModpackContent content = new ModpackContent("TestPack", null, testFilesDir, new HashSet<>(), new HashSet<>(editable), new HashSet<>(), new ModpackExecutor().getExecutor()); - content.create(null); + scanner.scanAndGenerate(); + Set generatedItems = scanner.getGroupFields().files; boolean correct = true; - System.out.println(); - if (content.list.size() != correctResults.size()) { - System.out.println("Incorrect number of items! Expected " + correctResults.size() + " but got " + content.list.size()); + if (generatedItems.size() != correctResults.size()) { + System.out.println("Incorrect number of items! Expected " + correctResults.size() + " but got " + generatedItems.size()); correct = false; } - for (var item : content.list) { + for (var item : generatedItems) { if (correctResults.contains(item.toString())) { System.out.println("Correct: " + item); } else { @@ -122,6 +148,6 @@ void modpackTest() { } assertTrue(correct); + executor.shutdown(); } - } \ No newline at end of file diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java index 5c77d010f..012e37b0a 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/Preload.java @@ -3,8 +3,8 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.ConfigTools; -import pl.skidam.automodpack_core.config.ConfigUtils; import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.modpack.ClientSelectionManager; import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.client.ModpackUtils; @@ -13,7 +13,6 @@ import pl.skidam.automodpack_loader_core.mods.ModpackLoader; import java.io.IOException; -import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.PosixFilePermission; @@ -41,7 +40,8 @@ public Preload() { } private void updateAll() { - var optionalSelectedModpackDir = ModpackContentTools.getModpackDir(clientConfig.selectedModpack); + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + var optionalSelectedModpackDir = ModpackContentTools.getModpackDir(clientSelectionManager.getSelectedPackId()); if (LOADER_MANAGER.getEnvironmentType() == LoaderManagerService.EnvironmentType.SERVER || optionalSelectedModpackDir.isEmpty()) { SelfUpdater.update(); @@ -49,24 +49,15 @@ private void updateAll() { } selectedModpackDir = optionalSelectedModpackDir.get(); - InetSocketAddress selectedModpackAddress = null; - InetSocketAddress selectedServerAddress = null; - boolean requiresMagic = true; // Default to true - if (!clientConfig.selectedModpack.isBlank() && clientConfig.installedModpacks.containsKey(clientConfig.selectedModpack)) { - var entry = clientConfig.installedModpacks.get(clientConfig.selectedModpack); - selectedModpackAddress = entry.hostAddress; - selectedServerAddress = entry.serverAddress; - requiresMagic = entry.requiresMagic; - } + Jsons.ModpackAddresses modpackAddresses = clientSelectionManager.getSelectedAddresses(); // Only selfupdate if no modpack is selected - if (selectedModpackAddress == null) { + if (modpackAddresses.hostAddress == null) { SelfUpdater.update(); LegacyClientCacheUtils.deleteDummyFiles(); } else { - Secrets.Secret secret = SecretsStore.getClientSecret(clientConfig.selectedModpack); + Secrets.Secret secret = SecretsStore.getClientSecret(clientSelectionManager.getSelectedPackId()); - Jsons.ModpackAddresses modpackAddresses = new Jsons.ModpackAddresses(selectedModpackAddress, selectedServerAddress, requiresMagic); var optionalLatestModpackContent = ModpackUtils.requestServerModpackContent(modpackAddresses, secret, false); var latestModpackContent = ConfigTools.loadModpackContent(selectedModpackDir.resolve(hostModpackContentFile.getFileName())); @@ -120,65 +111,68 @@ private void initializeConstants() { private void loadConfigs() { long startTime = System.currentTimeMillis(); + // TODO V3 migration // load client config - if (clientConfigOverride == null) { - var clientConfigVersion = ConfigTools.softLoad(clientConfigFile, Jsons.VersionConfigField.class); - if (clientConfigVersion != null) { - if (clientConfigVersion.DO_NOT_CHANGE_IT == 1) { - // Update the configs schemes to not crash the game if loaded with old config! - var clientConfigV1 = ConfigTools.load(clientConfigFile, Jsons.ClientConfigFieldsV1.class); - if (clientConfigV1 != null) { // update to V2 - just delete the installedModpacks - clientConfigVersion.DO_NOT_CHANGE_IT = 2; - clientConfigV1.DO_NOT_CHANGE_IT = 2; - clientConfigV1.installedModpacks = null; - } - - ConfigTools.save(clientConfigFile, clientConfigV1); - LOGGER.info("Updated client config version to {}", clientConfigVersion.DO_NOT_CHANGE_IT); - } - } - - clientConfig = ConfigTools.load(clientConfigFile, Jsons.ClientConfigFieldsV2.class); - } else { - // TODO: when connecting to the new server which provides modpack different modpack, ask the user if they want, stop using overrides - LOGGER.warn("You are using unofficial {} mod", MOD_ID); - LOGGER.warn("Using client config overrides! Editing the {} file will have no effect", clientConfigFile); - LOGGER.warn("Remove the {} file from inside the jar or remove and download fresh {} mod jar from modrinth/curseforge", clientConfigFileOverrideResource, MOD_ID); - clientConfig = ConfigTools.load(clientConfigOverride, Jsons.ClientConfigFieldsV2.class); - } - - var serverConfigVersion = ConfigTools.softLoad(serverConfigFile, Jsons.VersionConfigField.class); - if (serverConfigVersion != null) { - if (serverConfigVersion.DO_NOT_CHANGE_IT == 1) { - // Update the configs schemes to make this update not as breaking as it could be - var serverConfigV1 = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV1.class); - var serverConfigV2 = ConfigTools.softLoad(serverConfigFile, Jsons.ServerConfigFieldsV2.class); - if (serverConfigV1 != null && serverConfigV2 != null) { - serverConfigVersion.DO_NOT_CHANGE_IT = 2; - serverConfigV2.DO_NOT_CHANGE_IT = 2; - - if (serverConfigV1.hostIp.isBlank()) { - serverConfigV2.addressToSend = ""; - } else { - serverConfigV2.addressToSend = AddressHelpers.parse(serverConfigV1.hostIp).getHostString(); - } - - if (serverConfigV1.hostModpackOnMinecraftPort) { - serverConfigV2.bindPort = -1; - serverConfigV2.portToSend = -1; - } else { - serverConfigV2.bindPort = serverConfigV1.hostPort; - serverConfigV2.portToSend = serverConfigV1.hostPort; - } - } - - ConfigTools.save(serverConfigFile, serverConfigV2); - LOGGER.info("Updated server config version to {}", serverConfigVersion.DO_NOT_CHANGE_IT); - } - } +// if (clientConfigOverride == null) { +// var clientConfigVersion = ConfigTools.softLoad(clientConfigFile, Jsons.VersionConfigField.class); +// if (clientConfigVersion != null) { +// if (clientConfigVersion.DO_NOT_CHANGE_IT == 1) { +// // Update the configs schemes to not crash the game if loaded with old config! +// var clientConfigV1 = ConfigTools.load(clientConfigFile, Jsons.ClientConfigFieldsV1.class); +// if (clientConfigV1 != null) { // update to V2 - just delete the installedModpacks +// clientConfigVersion.DO_NOT_CHANGE_IT = 2; +// clientConfigV1.DO_NOT_CHANGE_IT = 2; +// clientConfigV1.installedModpacks = null; +// } +// +// ConfigTools.save(clientConfigFile, clientConfigV1); +// LOGGER.info("Updated client config version to {}", clientConfigVersion.DO_NOT_CHANGE_IT); +// } +// } +// +// clientConfig = ConfigTools.load(clientConfigFile, Jsons.ClientConfigFieldsV2.class); +// } else { +// // TODO: when connecting to the new server which provides modpack different modpack, ask the user if they want, stop using overrides +// LOGGER.warn("You are using unofficial {} mod", MOD_ID); +// LOGGER.warn("Using client config overrides! Editing the {} file will have no effect", clientConfigFile); +// LOGGER.warn("Remove the {} file from inside the jar or remove and download fresh {} mod jar from modrinth/curseforge", clientConfigFileOverrideResource, MOD_ID); +// clientConfig = ConfigTools.load(clientConfigOverride, Jsons.ClientConfigFieldsV2.class); +// } +// +// var serverConfigVersion = ConfigTools.softLoad(serverConfigFile, Jsons.VersionConfigField.class); +// if (serverConfigVersion != null) { +// if (serverConfigVersion.DO_NOT_CHANGE_IT == 1) { +// // Update the configs schemes to make this update not as breaking as it could be +// var serverConfigV1 = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV1.class); +// var serverConfigV2 = ConfigTools.softLoad(serverConfigFile, Jsons.ServerConfigFieldsV2.class); +// if (serverConfigV1 != null && serverConfigV2 != null) { +// serverConfigVersion.DO_NOT_CHANGE_IT = 2; +// serverConfigV2.DO_NOT_CHANGE_IT = 2; +// +// if (serverConfigV1.hostIp.isBlank()) { +// serverConfigV2.addressToSend = ""; +// } else { +// serverConfigV2.addressToSend = AddressHelpers.parse(serverConfigV1.hostIp).getHostString(); +// } +// +// if (serverConfigV1.hostModpackOnMinecraftPort) { +// serverConfigV2.bindPort = -1; +// serverConfigV2.portToSend = -1; +// } else { +// serverConfigV2.bindPort = serverConfigV1.hostPort; +// serverConfigV2.portToSend = serverConfigV1.hostPort; +// } +// } +// +// ConfigTools.save(serverConfigFile, serverConfigV2); +// LOGGER.info("Updated server config version to {}", serverConfigVersion.DO_NOT_CHANGE_IT); +// } +// } + + clientConfig = ConfigTools.load(clientConfigFile, Jsons.ClientConfigFieldsV3.class); // load server config - serverConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV2.class); + serverConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV3.class); if (serverConfig != null) { // Add current loader to the list @@ -194,25 +188,25 @@ private void loadConfigs() { LOGGER.info("Changed modpack name to {}", serverConfig.modpackName); } - ConfigUtils.normalizeServerConfig(serverConfig); +// ConfigUtils.normalizeServerConfig(serverConfig); // Save changes ConfigTools.save(serverConfigFile, serverConfig); } - if (clientConfig != null) { - // Very important to have this map initialized - if (clientConfig.installedModpacks == null) { - clientConfig.installedModpacks = new HashMap<>(); - } - - if (clientConfig.selectedModpack == null) { - clientConfig.selectedModpack = ""; - } - - // Save changes - ConfigTools.save(clientConfigFile, clientConfig); - } +// if (clientConfig != null) { +// // Very important to have this map initialized +// if (clientConfig.installedModpacks == null) { +// clientConfig.installedModpacks = new HashMap<>(); +// } +// +// if (clientConfig.selectedModpack == null) { +// clientConfig.selectedModpack = ""; +// } +// +// // Save changes +// ConfigTools.save(clientConfigFile, clientConfig); +// } knownHosts = ConfigTools.load(knownHostsFile, Jsons.KnownHostsFields.class); if (knownHosts != null) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java index 62077c532..749519aed 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java @@ -39,7 +39,7 @@ public static boolean update() { return update(null); } - public static boolean update(Jsons.ModpackContentFields serverModpackContent) { + public static boolean update(Jsons.ModpackContent serverModpackContent) { if (LOADER_MANAGER.isDevelopmentEnvironment()) return false; if (LOADER_MANAGER.getEnvironmentType() == LoaderManagerService.EnvironmentType.SERVER && !serverConfig.selfUpdater) { diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java index 6607af5a1..087abf9f9 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java @@ -5,6 +5,7 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_core.config.ConfigTools; +import pl.skidam.automodpack_core.modpack.ClientSelectionManager; import pl.skidam.automodpack_core.protocol.DownloadClient; import pl.skidam.automodpack_core.utils.*; import pl.skidam.automodpack_core.utils.cache.FileMetadataCache; @@ -31,9 +32,9 @@ public class ModpackUpdater { public DownloadManager downloadManager; public long totalBytesToDownload = 0; public boolean fullDownload = false; - private Jsons.ModpackContentFields serverModpackContent; + private Jsons.ModpackContent serverModpackContent; private String serverModpackContentJson; // TODO: remove this variable and use serverModpackContent directly - public Map> failedDownloads = new HashMap<>(); + public Map> failedDownloads = new HashMap<>(); private final Set newDownloadedFiles = new HashSet<>(); // Only files which did not exist before. Because some files may have the same name/path and be updated. private final Jsons.ModpackAddresses modpackAddresses; private final Secrets.Secret modpackSecret; @@ -44,11 +45,15 @@ public String getModpackName() { return serverModpackContent.modpackName; } - public Set getModpackFileList() { - return serverModpackContent.list; + public Set getWholeModpackFileList() { + return ModpackUtils.getModpackFileList(serverModpackContent); } - public ModpackUpdater(Jsons.ModpackContentFields modpackContent, Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, Path modpackPath) { + public Jsons.ModpackContent getServerModpackContent() { + return serverModpackContent; + } + + public ModpackUpdater(Jsons.ModpackContent modpackContent, Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, Path modpackPath) { this.serverModpackContent = modpackContent; this.modpackAddresses = modpackAddresses; this.modpackSecret = secret; @@ -82,7 +87,7 @@ public void processModpackUpdate(ModpackUtils.UpdateCheckResult result) { // Handle new modpack if (!Files.exists(modpackContentFile)) { if (preload) { - startUpdate(serverModpackContent.list); + startUpdate(ModpackUtils.getModpackFileList(serverModpackContent)); } else { fullDownload = true; new ScreenManager().danger(new ScreenManager().getScreen().orElseThrow(), this); @@ -167,7 +172,7 @@ private void CheckAndLoadModpack(FileMetadataCache cache) throws Exception { LOGGER.info("Modpack is already loaded"); } - public void startUpdate(Set filesToUpdate) { + public void startUpdate(Set filesToUpdate) { if (modpackSecret == null) { LOGGER.error("Cannot update modpack, secret is null"); return; @@ -190,9 +195,9 @@ public void startUpdate(Set files long startFetching = System.currentTimeMillis(); List fetchDatas = new ArrayList<>(); - for (Jsons.ModpackContentFields.ModpackContentItem serverItem : finalFilesToUpdate) { + for (Jsons.ModpackContentItem serverItem : finalFilesToUpdate) { - totalBytesToDownload += Long.parseLong(serverItem.size); + totalBytesToDownload += serverItem.size; String fileType = serverItem.type; // Check if the file is mod, shaderpack or resourcepack is available to download from modrinth or curseforge @@ -255,7 +260,7 @@ public void startUpdate(Set files } } - private void downloadModpack(Set finalFilesToUpdate, long startFetching, @Nullable FetchManager fetchManager, FileMetadataCache cache) throws InterruptedException { + private void downloadModpack(Set finalFilesToUpdate, long startFetching, @Nullable FetchManager fetchManager, FileMetadataCache cache) throws InterruptedException { int wholeQueue = finalFilesToUpdate.size(); if (wholeQueue == 0) { @@ -279,7 +284,7 @@ private void downloadModpack(Set String serverFilePath = serverItem.file; String serverFileHash = serverItem.sha1; - long serverFileSize = Long.parseLong(serverItem.size); + long serverFileSize = serverItem.size; Path downloadFile = SmartFileUtils.getPath(modpackDir, serverFilePath); @@ -336,7 +341,7 @@ private void downloadModpack(Set failedDownloadsSecMap.forEach((k, v) -> { hashesToRefresh.put(k.file, k.sha1); failedDownloads.remove(k); - totalBytesToDownload += Long.parseLong(k.size); + totalBytesToDownload += k.size; }); if (hashesToRefresh.isEmpty()) { @@ -368,7 +373,7 @@ private void downloadModpack(Set this.serverModpackContentJson = GSON.toJson(refreshedContent); // filter list to only the failed downloads - var refreshedFilteredList = refreshedContent.list.stream().filter(item -> hashesToRefresh.containsKey(item.file)).toList(); + var refreshedFilteredList = ModpackUtils.getModpackFileList(refreshedContent).stream().filter(item -> hashesToRefresh.containsKey(item.file)).toList(); if (refreshedFilteredList.isEmpty()) { return; } @@ -388,7 +393,7 @@ private void downloadModpack(Set String serverFilePath = serverItem.file; String serverFileHash = serverItem.sha1; - long serverFileSize = Long.parseLong(serverItem.size); + long serverFileSize = serverItem.size; Path downloadFile = SmartFileUtils.getPath(modpackDir, serverFilePath); @@ -429,11 +434,11 @@ private void downloadModpack(Set private boolean applyModpack(FileMetadataCache cache) throws Exception { ModpackUtils.selectModpack(modpackDir, modpackAddresses, newDownloadedFiles); try { // try catch this error there because we don't want to stop the whole method just because of that - SecretsStore.saveClientSecret(clientConfig.selectedModpack, modpackSecret); + SecretsStore.saveClientSecret(ClientSelectionManager.getMgr().getSelectedPackId(), modpackSecret); } catch (IllegalArgumentException e) { LOGGER.error("Failed to save client secret", e); } - Jsons.ModpackContentFields modpackContent = ConfigTools.loadModpackContent(modpackContentFile); + Jsons.ModpackContent modpackContent = ConfigTools.loadModpackContent(modpackContentFile); if (modpackContent == null) { throw new IllegalStateException("Failed to load modpack content"); // Something gone very wrong... @@ -448,7 +453,7 @@ private boolean applyModpack(FileMetadataCache cache) throws Exception { boolean needsRestart0 = deleteNonModpackFiles(modpackContent, cache); Set workaroundMods = new WorkaroundUtil(modpackDir).getWorkaroundMods(modpackContent); - Set filesNotToCopy = getFilesNotToCopy(modpackContent.list, workaroundMods); + Set filesNotToCopy = getFilesNotToCopy(ModpackUtils.getModpackFileList(modpackContent), workaroundMods); boolean needsRestart1 = ModpackUtils.correctFilesLocations(modpackDir, modpackContent, filesNotToCopy, cache); Set modpackMods = new HashSet<>(); @@ -496,7 +501,7 @@ private boolean applyModpack(FileMetadataCache cache) throws Exception { ignoredFiles = ModpackUtils.getIgnoredFiles(conflictingNestedMods, workaroundMods); } - Set forceCopyFiles = modpackContent.list.stream() + Set forceCopyFiles = ModpackUtils.getModpackFileList(modpackContent).stream() .filter(item -> item.forceCopy) .map(item -> item.file) .collect(Collectors.toSet()); @@ -516,11 +521,11 @@ private boolean applyModpack(FileMetadataCache cache) throws Exception { } // returns set of formated files which we should not copy to the cwd - let them stay in the modpack directory - private Set getFilesNotToCopy(Set modpackContentItems, Set workaroundMods) { + private Set getFilesNotToCopy(Set modpackContentItems, Set workaroundMods) { Set filesNotToCopy = new HashSet<>(); // Make list of files which we do not copy to the running directory - for (Jsons.ModpackContentFields.ModpackContentItem item : modpackContentItems) { + for (Jsons.ModpackContentItem item : modpackContentItems) { if (item.forceCopy) { continue; } @@ -540,8 +545,8 @@ private Set getFilesNotToCopy(Set modpackFiles = modpackContent.list.stream().map(modpackContentField -> modpackContentField.file).collect(Collectors.toSet()); + private boolean deleteNonModpackFiles(Jsons.ModpackContent modpackContent, FileMetadataCache cache) throws IOException { + Set modpackFiles = ModpackUtils.getModpackFileList(modpackContent).stream().map(modpackContentField -> modpackContentField.file).collect(Collectors.toSet()); List pathList; try (Stream pathStream = Files.walk(modpackDir)) { pathList = pathStream.toList(); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java index 73151268c..9981b108e 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUtils.java @@ -4,6 +4,7 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.modpack.ClientSelectionManager; import pl.skidam.automodpack_core.protocol.DownloadClient; import pl.skidam.automodpack_core.protocol.NetUtils; import pl.skidam.automodpack_core.utils.LegacyClientCacheUtils; @@ -34,34 +35,34 @@ public class ModpackUtils { // Modpack may require update even if there's no files to update, because some files may need to be deleted - public record UpdateCheckResult(boolean requiresUpdate, Set filesToUpdate) {} + public record UpdateCheckResult(boolean requiresUpdate, Set filesToUpdate) {} // Fast and friendly method to check if the modpack is up to date without modifying anything on disk - public static UpdateCheckResult isUpdate(Jsons.ModpackContentFields serverModpackContent, Path modpackDir) { - if (serverModpackContent == null || serverModpackContent.list == null) { - throw new IllegalArgumentException("Server modpack content list is null"); + public static UpdateCheckResult isUpdate(Jsons.ModpackContent serverModpackContent, Path modpackDir) { + if (serverModpackContent == null || serverModpackContent.groups == null) { + throw new IllegalArgumentException("Server modpack content groups is null"); } var optionalClientModpackContentFile = ModpackContentTools.getModpackContentFile(modpackDir); if (optionalClientModpackContentFile.isEmpty() || !Files.exists(optionalClientModpackContentFile.get())) { - return new UpdateCheckResult(true, serverModpackContent.list); + return new UpdateCheckResult(true, getModpackFileList(serverModpackContent)); } - Jsons.ModpackContentFields clientModpackContent = ConfigTools.loadModpackContent(optionalClientModpackContentFile.get()); + Jsons.ModpackContent clientModpackContent = ConfigTools.loadModpackContent(optionalClientModpackContentFile.get()); if (clientModpackContent == null) { - return new UpdateCheckResult(true, serverModpackContent.list); + return new UpdateCheckResult(true, getModpackFileList(serverModpackContent)); } LOGGER.info("Verifying content against server list..."); var start = System.currentTimeMillis(); - Set filesToUpdate = ConcurrentHashMap.newKeySet(); + Set filesToUpdate = ConcurrentHashMap.newKeySet(); // Group & Sort Server Files (Optimizes Disk Seek Pattern) // Grouping by parent folder ensures we process the disk sequentially (Dir A, then Dir B). // TreeMap ensures alphabetical order of directories (HDD friendly). - Map> itemsByDir = - serverModpackContent.list.stream() + Map> itemsByDir = + getModpackFileList(serverModpackContent).stream() .collect(Collectors.groupingBy( item -> SmartFileUtils.getPath(modpackDir, item.file).getParent(), TreeMap::new, @@ -71,9 +72,9 @@ public static UpdateCheckResult isUpdate(Jsons.ModpackContentFields serverModpac try (var cache = FileMetadataCache.open(hashCacheDBFile)) { // Process Directory by Directory - for (Map.Entry> entry : itemsByDir.entrySet()) { + for (Map.Entry> entry : itemsByDir.entrySet()) { Path parentDir = entry.getKey(); - List itemsInDir = entry.getValue(); + List itemsInDir = entry.getValue(); // If directory is missing, all items in it are missing. if (!Files.exists(parentDir)) { @@ -120,7 +121,7 @@ public FileVisitResult visitFileFailed(@NotNull Path file, @NotNull IOException } // Check Size first from already read attributes - if (diskAttrs.size() != Long.parseLong(serverItem.size)) { + if (diskAttrs.size() != serverItem.size) { filesToUpdate.add(serverItem); continue; } @@ -138,7 +139,7 @@ public FileVisitResult visitFileFailed(@NotNull Path file, @NotNull IOException } catch (Exception e) { LOGGER.error("Error during update check", e); // Fail-safe: assume update needed if process crashes - return new UpdateCheckResult(true, serverModpackContent.list); + return new UpdateCheckResult(true, getModpackFileList(serverModpackContent)); } if (!filesToUpdate.isEmpty()) { @@ -148,11 +149,11 @@ public FileVisitResult visitFileFailed(@NotNull Path file, @NotNull IOException LOGGER.info("Checking for deleted files..."); - Set serverFileSet = serverModpackContent.list.stream() + Set serverFileSet = getModpackFileList(serverModpackContent).stream() .map(item -> item.file) .collect(Collectors.toSet()); - for (Jsons.ModpackContentFields.ModpackContentItem clientItem : clientModpackContent.list) { + for (Jsons.ModpackContentItem clientItem : getModpackFileList(serverModpackContent)) { if (!serverFileSet.contains(clientItem.file)) { LOGGER.info("Found file marked for deletion: {}", clientItem.file); return new UpdateCheckResult(true, Set.of()); @@ -163,8 +164,16 @@ public FileVisitResult visitFileFailed(@NotNull Path file, @NotNull IOException return new UpdateCheckResult(false, Set.of()); } + public static Set getModpackFileList(Jsons.ModpackContent serverModpackContent) { + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + if (clientSelectionManager.packExists(serverModpackContent.modpackName)) { + return clientSelectionManager.getSelectedFiles(serverModpackContent); + } + return serverModpackContent.groups.values().stream().flatMap(group -> group.files.stream()).collect(Collectors.toSet()); + } + // Scans for files missing from the store. If found in the CWD (and the hash matches), copies them to the store. - public static void populateStoreFromCWD(Set filesToUpdate, FileMetadataCache cache) { + public static void populateStoreFromCWD(Set filesToUpdate, FileMetadataCache cache) { for (var entry : filesToUpdate) { Path storeFile = SmartFileUtils.getPath(storeDir, entry.sha1); @@ -189,8 +198,8 @@ public static void populateStoreFromCWD(Set identifyUncachedFiles(Set filesToCheck) { - Set nonExistingFiles = new HashSet<>(); + public static Set identifyUncachedFiles(Set filesToCheck) { + Set nonExistingFiles = new HashSet<>(); for (var entry : filesToCheck) { Path storeFile = SmartFileUtils.getPath(storeDir, entry.sha1); @@ -203,8 +212,8 @@ public static Set identifyUncache // Installs files from the store (storeDir/) to the instance (modpackDir/). // Attempts to hardlink first, falls back to a copy if that fails. - public static void hardlinkModpack(Path modpackDir, Jsons.ModpackContentFields serverModpackContent, FileMetadataCache cache) throws IOException { - for (Jsons.ModpackContentFields.ModpackContentItem contentItem : serverModpackContent.list) { + public static void hardlinkModpack(Path modpackDir, Jsons.ModpackContent serverModpackContent, FileMetadataCache cache) throws IOException { + for (Jsons.ModpackContentItem contentItem : getModpackFileList(serverModpackContent)) { String formattedFile = contentItem.file; Path modpackFile = SmartFileUtils.getPath(modpackDir, formattedFile); Path storeFile = SmartFileUtils.getPath(storeDir, contentItem.sha1); @@ -227,7 +236,7 @@ public static void hardlinkModpack(Path modpackDir, Jsons.ModpackContentFields s } } - public static boolean deleteFilesMarkedForDeletionByTheServer(Set filesToDeleteOnClient, FileMetadataCache cache) { + public static boolean deleteFilesMarkedForDeletionByTheServer(Set filesToDeleteOnClient, FileMetadataCache cache) { if (!clientConfig.allowRemoteNonModpackDeletions) { if (!filesToDeleteOnClient.isEmpty()) { LOGGER.warn("Server requested deletion of {} files, but remote deletions are disabled in client config! Consider deleting them manually.", filesToDeleteOnClient.size()); @@ -300,11 +309,11 @@ public static boolean deleteFilesMarkedForDeletionByTheServer(Set filesNotToCopy, FileMetadataCache cache) throws IOException { + public static boolean correctFilesLocations(Path modpackDir, Jsons.ModpackContent serverModpackContent, Set filesNotToCopy, FileMetadataCache cache) throws IOException { boolean needsRestart = false; // correct the files locations - for (Jsons.ModpackContentFields.ModpackContentItem contentItem : serverModpackContent.list) { + for (Jsons.ModpackContentItem contentItem : getModpackFileList(serverModpackContent)) { String formattedFile = contentItem.file; Path modpackFile = SmartFileUtils.getPath(modpackDir, formattedFile); Path runFile = SmartFileUtils.getPathFromCWD(formattedFile); @@ -349,10 +358,10 @@ public static boolean correctFilesLocations(Path modpackDir, Jsons.ModpackConten return needsRestart; } - public static boolean removeRestModsNotToCopy(Jsons.ModpackContentFields serverModpackContent, Set filesNotToCopy, Set modsToKeep, FileMetadataCache cache) { + public static boolean removeRestModsNotToCopy(Jsons.ModpackContent serverModpackContent, Set filesNotToCopy, Set modsToKeep, FileMetadataCache cache) { boolean needsRestart = false; - for (Jsons.ModpackContentFields.ModpackContentItem contentItem : serverModpackContent.list) { + for (Jsons.ModpackContentItem contentItem : getModpackFileList(serverModpackContent)) { String formattedFile = contentItem.file; Path runFile = SmartFileUtils.getPathFromCWD(formattedFile); boolean isMod = "mod".equals(contentItem.type); @@ -515,11 +524,13 @@ private static void addDependenciesRecursively(FileInspection.Mod mod, Collectio } } - public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackContent, Path modpackDir) { - String currentName = clientConfig.selectedModpack; + public static Path renameModpackDir(Jsons.ModpackContent serverModpackContent, Path modpackDir) { + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + + String currentName = clientSelectionManager.getSelectedPackId(); String newName = serverModpackContent.modpackName; - if (clientConfig.installedModpacks == null || clientConfig.selectedModpack == null || clientConfig.selectedModpack.isBlank()) { + if (currentName == null || currentName.isBlank()) { return modpackDir; } @@ -527,7 +538,7 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont return modpackDir; } - var installedAddresses = clientConfig.installedModpacks.get(currentName); + var installedAddresses = clientSelectionManager.getSelectedAddresses(); if (installedAddresses == null) { return modpackDir; } @@ -554,8 +565,10 @@ public static Path renameModpackDir(Jsons.ModpackContentFields serverModpackCont // Returns true if value changed public static boolean selectModpack(Path modpackDirToSelect, Jsons.ModpackAddresses modpackAddresses, Set newDownloadedFiles) { + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + + String oldName = clientSelectionManager.getSelectedPackId(); String newName = modpackDirToSelect.getFileName().toString(); - String oldName = clientConfig.selectedModpack; // If nothing changed, update list only and return early to avoid I/O. if (Objects.equals(newName, oldName)) { @@ -575,9 +588,7 @@ public static boolean selectModpack(Path modpackDirToSelect, Jsons.ModpackAddres processEditableFiles(modpackDirToSelect, (dir, files) -> ModpackUtils.copyPreviousEditableFiles(dir, files, newDownloadedFiles)); - // Update Configuration and Save - clientConfig.selectedModpack = newName; - ConfigTools.save(clientConfigFile, clientConfig); + clientSelectionManager.setSelectedPack(newName); ModpackUtils.addModpackToList(newName, modpackAddresses); LOGGER.info("Selected modpack: {}", newName); @@ -587,37 +598,40 @@ public static boolean selectModpack(Path modpackDirToSelect, Jsons.ModpackAddres private static void processEditableFiles(Path modpackDir, java.util.function.BiConsumer> action) { Path contentFile = modpackDir.resolve(hostModpackContentFile.getFileName()); - Jsons.ModpackContentFields content = ConfigTools.loadModpackContent(contentFile); + Jsons.ModpackContent content = ConfigTools.loadModpackContent(contentFile); if (content != null) { - Set editableFiles = getEditableFiles(content.list); + Set editableFiles = getEditableFiles(getModpackFileList(content)); action.accept(modpackDir, editableFiles); } } - public static void removeModpackFromList(String modpackName) { - if (modpackName == null || modpackName.isEmpty()) { + public static void removeModpackFromList(String modpackId) { + if (modpackId == null || modpackId.isEmpty()) { return; } - if (clientConfig.installedModpacks != null && clientConfig.installedModpacks.containsKey(modpackName)) { - Map modpacks = new HashMap<>(clientConfig.installedModpacks); - modpacks.remove(modpackName); - clientConfig.installedModpacks = modpacks; - ConfigTools.save(clientConfigFile, clientConfig); + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + + if (clientSelectionManager.packExists(modpackId)) { + clientSelectionManager.removePack(modpackId); } } - public static void addModpackToList(String modpackName, Jsons.ModpackAddresses modpackAddresses) { - if (modpackName == null || modpackName.isEmpty() || modpackAddresses.isAnyEmpty()) { + public static void addModpackToList(String packId, Jsons.ModpackAddresses modpackAddresses) { + if (packId == null || packId.isEmpty() || modpackAddresses.isAnyEmpty()) { return; } - Map modpacks = new HashMap<>(clientConfig.installedModpacks); - modpacks.put(modpackName, modpackAddresses); - clientConfig.installedModpacks = modpacks; + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + + var pack = new Jsons.ClientSelectionManagerFields.Modpack(modpackAddresses); + + if (clientSelectionManager.packExists(packId)) { + pack.selectedGroups = clientSelectionManager.getSelectedGroups(packId); + } - ConfigTools.save(clientConfigFile, clientConfig); + clientSelectionManager.addPack(packId, pack); } // Returns modpack name formatted for path or url if server doesn't provide modpack name @@ -645,19 +659,19 @@ public static Path getModpackPath(InetSocketAddress address, String modpackName) return modpackDir; } - public static Optional requestServerModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, boolean allowAskingUser) { + public static Optional requestServerModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, boolean allowAskingUser) { return fetchModpackContent(modpackAddresses, secret, (client) -> client.downloadFile(new byte[0], modpackContentTempFile, null), "Fetched", allowAskingUser); } - public static Optional refreshServerModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, byte[][] fileHashes, boolean allowAskingUser) { + public static Optional refreshServerModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, byte[][] fileHashes, boolean allowAskingUser) { return fetchModpackContent(modpackAddresses, secret, (client) -> client.requestRefresh(fileHashes, modpackContentTempFile), "Re-fetched", allowAskingUser); } - private static Optional fetchModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, Function> operation, String fetchType, boolean allowAskingUser) { + private static Optional fetchModpackContent(Jsons.ModpackAddresses modpackAddresses, Secrets.Secret secret, Function> operation, String fetchType, boolean allowAskingUser) { if (secret == null) return Optional.empty(); if (modpackAddresses.isAnyEmpty()) @@ -753,17 +767,17 @@ private static Boolean askUserAboutCertificate(InetSocketAddress address, String return accepted.get(); } - public static boolean potentiallyMalicious(Jsons.ModpackContentFields serverModpackContent) { + public static boolean potentiallyMalicious(Jsons.ModpackContent serverModpackContent) { if (isUnsafePath(serverModpackContent.modpackName, true)) { LOGGER.error("Modpack content is invalid: modpack name '{}' is unsafe/malicious", serverModpackContent.modpackName); return true; } - if (serverModpackContent.list == null || serverModpackContent.list.isEmpty()) { + if (serverModpackContent.groups == null || serverModpackContent.groups.isEmpty()) { return false; } - boolean listInvalid = serverModpackContent.list.stream().anyMatch(item -> { + boolean listInvalid = getModpackFileList(serverModpackContent).stream().anyMatch(item -> { if (isHashInvalid(item.sha1)) { LOGGER.error("Modpack content is invalid: file '{}' has invalid sha1 '{}'", item.file, item.sha1); return true; @@ -868,10 +882,10 @@ public static void copyPreviousEditableFiles(Path modpackDir, Set editab } } - static Set getEditableFiles(Set modpackContentItems) { + static Set getEditableFiles(Set modpackContentItems) { Set editableFiles = new HashSet<>(); - for (Jsons.ModpackContentFields.ModpackContentItem modpackContentItem : modpackContentItems) { + for (Jsons.ModpackContentItem modpackContentItem : modpackContentItems) { if (modpackContentItem.editable) { editableFiles.add(modpackContentItem.file); } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/PreloadScreenImpl.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/PreloadScreenImpl.java index 3628edc6e..b2b507c2f 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/PreloadScreenImpl.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/PreloadScreenImpl.java @@ -8,6 +8,9 @@ public class PreloadScreenImpl implements ScreenService { @Override public void download(Object... args) { } + @Override + public void modpackSelection(Object... args) { } + @Override public void fetch(Object... args) { } diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenManager.java index 02a7ff3f3..d9489a035 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenManager.java @@ -11,6 +11,11 @@ public void download(Object... args) { INSTANCE.download(args); } + @Override + public void modpackSelection(Object... args) { + INSTANCE.modpackSelection(args); + } + @Override public void fetch(Object... args) { INSTANCE.fetch(args); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenService.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenService.java index e7962bdb1..b8f9fa193 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenService.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/screen/ScreenService.java @@ -5,6 +5,7 @@ public interface ScreenService { void download(Object... args); + void modpackSelection(Object... args); void fetch(Object... args); void changelog(Object... args); void restart(Object... args); diff --git a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/FetchManager.java b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/FetchManager.java index 026b44958..485e8ac2b 100644 --- a/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/FetchManager.java +++ b/loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/FetchManager.java @@ -18,7 +18,7 @@ public class FetchManager { // Send request to CurseForge with murmurs // Return the results i guess - public record FetchData(String file, String sha1, String murmur, String fileSize, String fileType) { } + public record FetchData(String file, String sha1, String murmur, long fileSize, String fileType) { } public record FetchedData (List urls, List mainPageUrls) { } public record Datas(FetchData fetchData, FetchedData fetchedData) { } private final Map fetchDatas = new HashMap<>(); diff --git a/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java b/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java index bd116e232..7f8389583 100644 --- a/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java +++ b/src/main/java/pl/skidam/automodpack/client/ScreenImpl.java @@ -1,6 +1,7 @@ package pl.skidam.automodpack.client; import pl.skidam.automodpack.client.ui.*; +import pl.skidam.automodpack_core.config.Jsons; import pl.skidam.automodpack_loader_core.client.Changelogs; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.screen.ScreenService; @@ -22,6 +23,11 @@ public void download(Object... args) { Minecraft.getInstance().execute(() -> Screens.download(args[0], args[1])); } + @Override + public void modpackSelection(Object... args) { + Minecraft.getInstance().execute(() -> Screens.modpackSelection(args[0], args[1], args[2])); + } + @Override public void fetch(Object... args) { Minecraft.getInstance().execute(() -> Screens.fetch(args[0])); @@ -87,6 +93,10 @@ public static void download(Object downloadManager, Object header) { Screens.setScreen(new DownloadScreen((DownloadManager) downloadManager, (String) header)); } + public static void modpackSelection(Object parentScreen, Object modpackUpdater, Object modpackcontent) { + Screens.setScreen(new ModpackSelectionScreen((Screen) parentScreen, (ModpackUpdater) modpackUpdater, (Jsons.ModpackContent) modpackcontent)); + } + public static void fetch(Object fetchManager) { Screens.setScreen(new FetchScreen((FetchManager) fetchManager)); } diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java index 087f6a77a..a8d235230 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/ChangelogScreen.java @@ -24,7 +24,7 @@ public class ChangelogScreen extends VersionedScreen { private final Path modpackDir; private final Changelogs changelogs; private static Map formattedChanges; - private Jsons.ModpackContentFields modpackContent = null; + private Jsons.ModpackContent modpackContent = null; private ListEntryWidget listEntryWidget; private EditBox searchField; private Button backButton; @@ -142,7 +142,7 @@ private void drawSummaryOfChanges(VersionedMatrices matrices) { String summary = "+ " + filesAdded + " | - " + filesRemoved; - drawCenteredTextWithShadow( + drawCenteredText( matrices, font, VersionedText.literal(summary), diff --git a/src/main/java/pl/skidam/automodpack/client/ui/DangerScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/DangerScreen.java index 3af3502cf..c5b0ecdbc 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/DangerScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/DangerScreen.java @@ -8,6 +8,9 @@ import pl.skidam.automodpack.client.ui.versioned.VersionedScreen; import pl.skidam.automodpack.client.ui.versioned.VersionedText; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; +import pl.skidam.automodpack_core.Constants; +import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_loader_core.screen.ScreenManager; public class DangerScreen extends VersionedScreen { @@ -45,10 +48,8 @@ protected void init() { this.height / 2 + 50, 120, 20, - VersionedText.translatable( - "automodpack.danger.confirm" - ).withStyle(ChatFormatting.BOLD), - button -> Util.backgroundExecutor().execute(() -> modpackUpdaterInstance.startUpdate(modpackUpdaterInstance.getModpackFileList())) + VersionedText.translatable("automodpack.danger.confirm").withStyle(ChatFormatting.BOLD), + button -> procced() ) ); } @@ -58,19 +59,17 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, int lineHeight = 12; // Consistent line spacing // Title - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, - VersionedText.translatable("automodpack.danger").withStyle( - ChatFormatting.BOLD - ), + VersionedText.translatable("automodpack.danger").withStyle(ChatFormatting.BOLD), this.width / 2, this.height / 2 - 60, TextColors.WHITE ); // Description line 1 - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.danger.description"), @@ -80,7 +79,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Description line 2 - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.danger.secDescription"), @@ -90,7 +89,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Description line 3 - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.danger.thiDescription"), @@ -100,10 +99,29 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); } + public void procced() { + try { + Jsons.ModpackContent content = modpackUpdaterInstance.getServerModpackContent(); + + if (content != null && content.groups != null && !content.groups.isEmpty()) { + // Modpack is valid and has groups, pass to selection UI + Util.backgroundExecutor().execute(() -> new ScreenManager().modpackSelection(this.parent, modpackUpdaterInstance, content)); + return; + } + } catch (Exception e) { + Constants.LOGGER.error("Failed to load modpack content for selection screen", e); + } + + Constants.LOGGER.error("Fallback?? Something went very wrong"); + + // Fallback or empty pack - just start raw update + Util.backgroundExecutor().execute(() -> modpackUpdaterInstance.startUpdate(modpackUpdaterInstance.getWholeModpackFileList())); + } + @Override public boolean onKeyPress(int keyCode, int scanCode, int modifiers) { if (keyCode == 257) { // Enter key (GLFW_KEY_ENTER = 257) - Util.backgroundExecutor().execute(() -> modpackUpdaterInstance.startUpdate(modpackUpdaterInstance.getModpackFileList())); + procced(); return true; } return super.onKeyPress(keyCode, scanCode, modifiers); @@ -111,6 +129,6 @@ public boolean onKeyPress(int keyCode, int scanCode, int modifiers) { @Override public boolean shouldCloseOnEsc() { - return false; + return true; } -} +} \ No newline at end of file diff --git a/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java index d10286042..0bc3b4e55 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/DownloadScreen.java @@ -41,7 +41,7 @@ public class DownloadScreen extends VersionedScreen { private String cachedETA = "Calculating..."; private long lastTextUpdate = 0; - private static final long TEXT_UPDATE_INTERVAL = 100; // Update strings 10x per second + private static final long TEXT_UPDATE_INTERVAL = 100; public DownloadScreen(DownloadManager downloadManager, String header) { super(VersionedText.literal("DownloadScreen")); @@ -132,24 +132,24 @@ private void drawDownloadingFiles(VersionedMatrices matrices) { matrices.scale(scale, scale, scale); if (downloadManager != null && !downloadManager.downloadsInProgress.isEmpty()) { - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.download.downloading").withStyle(ChatFormatting.BOLD), this.width / 2, y, TextColors.WHITE); int currentY = y + 15; synchronized (downloadManager.downloadsInProgress) { for (DownloadManager.DownloadData data : downloadManager.downloadsInProgress.values()) { - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.literal(data.getFileName()), (int) (((float) this.width / 2) * scale), currentY, TextColors.GRAY); currentY += 10; } } } else { - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.download.noFiles"), (int) (((float) this.width / 2) * scale), y, TextColors.WHITE); - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.wait").withStyle(ChatFormatting.BOLD), (int) (((float) this.width / 2) * scale), y + 24, TextColors.WHITE); } @@ -164,13 +164,13 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, drawDownloadingFiles(matrices); // Title - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.literal(header).withStyle(ChatFormatting.BOLD), this.width / 2, this.height / 2 - 110, TextColors.WHITE); if (downloadManager != null && downloadManager.isRunning()) { - drawCenteredTextWithShadow(matrices, this.font, (MutableComponent) getStage(), this.width / 2, this.height / 2 - 10, TextColors.WHITE); - drawCenteredTextWithShadow(matrices, this.font, (MutableComponent) getTotalETA(), this.width / 2, this.height / 2 - 10 + lineHeight * 2, TextColors.WHITE); + drawCenteredText(matrices, this.font, (MutableComponent) getStage(), this.width / 2, this.height / 2 - 10, TextColors.WHITE); + drawCenteredText(matrices, this.font, (MutableComponent) getTotalETA(), this.width / 2, this.height / 2 - 10 + lineHeight * 2, TextColors.WHITE); float scaleBar = 1.35F; int barWidth = PROGRESS_BAR_WIDTH; @@ -187,7 +187,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, drawTexture(PROGRESS_BAR_FULL_TEXTURE, matrices, Math.round(barDrawX), Math.round(barDrawY), 0, 0, Math.min(barFilledWidth, barWidth), barHeight, barWidth, barHeight); matrices.popPose(); - drawCenteredTextWithShadow(matrices, this.font, (MutableComponent) getTotalDownloadSpeed(), this.width / 2, this.height / 2 + 36 + lineHeight * 2, TextColors.WHITE); + drawCenteredText(matrices, this.font, (MutableComponent) getTotalDownloadSpeed(), this.width / 2, this.height / 2 + 36 + lineHeight * 2, TextColors.WHITE); cancelButton.active = true; } else { cancelButton.active = false; diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java index 2475d16e4..62423aa17 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/ErrorScreen.java @@ -38,7 +38,6 @@ private void initWidgets() { 20, VersionedText.translatable("automodpack.back"), button -> { - assert minecraft != null; minecraft.setScreen(null); } ); @@ -49,7 +48,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, int lineHeight = 12; // Consistent line spacing // Title with error indicator - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.literal("[AutoModpack] Error! ").append(VersionedText.translatable("automodpack.error").withStyle(ChatFormatting.RED)), this.width / 2, this.height / 2 - 50, @@ -58,7 +57,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, // Error messages for (int i = 0; i < this.errorMessages.length; i++) { - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable(this.errorMessages[i]), diff --git a/src/main/java/pl/skidam/automodpack/client/ui/FetchScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/FetchScreen.java index b82c6ab13..d748e3ff8 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/FetchScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/FetchScreen.java @@ -53,7 +53,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, } // Title - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.fetch").withStyle( @@ -65,7 +65,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Please wait message - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.wait"), @@ -75,7 +75,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Found count - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.fetch.found", getFetchesDone()), diff --git a/src/main/java/pl/skidam/automodpack/client/ui/FingerprintVerificationScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/FingerprintVerificationScreen.java index 6391c039f..4345b5fa0 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/FingerprintVerificationScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/FingerprintVerificationScreen.java @@ -125,32 +125,32 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, int lineHeight = 12; // Consistent line spacing // Title - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.title").withStyle(ChatFormatting.BOLD), this.width / 2, this.height / 2 - 85, TextColors.WHITE); // Description line 1 - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.description1"), this.width / 2, this.height / 2 - 65, TextColors.WHITE); // Description line 2 - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.description2"), this.width / 2, this.height / 2 - 65 + lineHeight, TextColors.WHITE); // Server fingerprint label - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.fingerprint.label"), this.width / 2, this.height / 2 - 35, TextColors.WHITE); // Server fingerprint value (concatenated, gray, not bold - intentionally harder to read) - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.literal(getConcatenatedFingerprint()), this.width / 2, this.height / 2 - 35 + lineHeight, TextColors.LIGHT_GRAY); // Confirmation text - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.confirm.text"), this.width / 2, this.height / 2 - 15 + lineHeight, TextColors.WHITE); } diff --git a/src/main/java/pl/skidam/automodpack/client/ui/ModpackSelectionScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/ModpackSelectionScreen.java new file mode 100644 index 000000000..d4ab50231 --- /dev/null +++ b/src/main/java/pl/skidam/automodpack/client/ui/ModpackSelectionScreen.java @@ -0,0 +1,347 @@ +package pl.skidam.automodpack.client.ui; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Util; +import pl.skidam.automodpack_loader_core.client.ModpackUpdater; +import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.modpack.ClientSelectionManager; +import pl.skidam.automodpack.client.ui.versioned.VersionedMatrices; +import pl.skidam.automodpack.client.ui.versioned.VersionedScreen; +import pl.skidam.automodpack.client.ui.versioned.VersionedText; +import pl.skidam.automodpack.client.ui.widget.ListEntry; +import pl.skidam.automodpack.client.ui.widget.ListEntryWidget; + +import java.util.*; + +/*? if >= 1.21.9 {*/ +import net.minecraft.client.input.MouseButtonEvent; +/*?}*/ + +public class ModpackSelectionScreen extends VersionedScreen { + + private final Screen parent; + private final ModpackUpdater updater; + private final Jsons.ModpackContent content; + + private GroupListWidget listWidget; + private boolean isValid = true; + private String validationError = ""; + + public final Set selectedGroups = new HashSet<>(); + + public ModpackSelectionScreen(Screen parent, ModpackUpdater updater, Jsons.ModpackContent content) { + super(VersionedText.literal("Select Modpack Groups")); + this.parent = parent; + this.updater = updater; + this.content = content; + + validatePack(); + if (isValid) { + initSelections(); + } + } + + @Override + protected void init() { + super.init(); + if (isValid) { + this.listWidget = new GroupListWidget(this.minecraft, this.width, this.height, 40, this.height - 40, 30); + + // Sort groups: 1. Req+Rec, 2. Req, 3. Rec, 4. Rest (Stable sort preserves definition order for ties) + List> sortedEntries = new ArrayList<>(content.groups.entrySet()); + sortedEntries.sort((e1, e2) -> { + Jsons.ModpackGroupFields g1 = e1.getValue(); + Jsons.ModpackGroupFields g2 = e2.getValue(); + + int score1 = (g1.required ? 2 : 0) + (g1.recommended ? 1 : 0); + int score2 = (g2.required ? 2 : 0) + (g2.recommended ? 1 : 0); + + if (score1 != score2) { + return Integer.compare(score2, score1); // Descending + } + + // Fallback to alphabetical sorting to ensure deterministic order (since HashMap scrambles it) + String name1 = g1.displayName != null ? g1.displayName : e1.getKey(); + String name2 = g2.displayName != null ? g2.displayName : e2.getKey(); + return name1.compareToIgnoreCase(name2); + }); + + // Populate list + for (Map.Entry entry : sortedEntries) { + this.listWidget.addEntry(new GroupEntry(entry.getKey(), entry.getValue())); + } + this.addRenderableWidget(this.listWidget); + + // Swapped Layout: + this.addRenderableWidget(buttonWidget(this.width / 2 - 105, this.height - 30, 100, 20, VersionedText.translatable("automodpack.danger.cancel"), button -> this.minecraft.setScreen(parent))); + this.addRenderableWidget(buttonWidget(this.width / 2 + 5, this.height - 30, 100, 20, VersionedText.literal("Install"), button -> install())); + } else { + // If invalid, just show Cancel in the middle + this.addRenderableWidget(buttonWidget(this.width / 2 - 50, this.height - 30, 100, 20, VersionedText.translatable("automodpack.danger.cancel"), button -> this.minecraft.setScreen(parent))); + } + } + + private void validatePack() { + for (Map.Entry entry : content.groups.entrySet()) { + String id = entry.getKey(); + Jsons.ModpackGroupFields group = entry.getValue(); + if (group.required) { + if (group.requires != null) { + for (String req : group.requires) { + if (!content.groups.containsKey(req)) { + isValid = false; + validationError = "Broken Pack: Required group '" + group.displayName + "' requires missing group '" + req + "'"; + return; + } + } + } + if (group.breaksWith != null) { + for (String conflict : group.breaksWith) { + Jsons.ModpackGroupFields conflictGroup = content.groups.get(conflict); + if (conflictGroup != null && conflictGroup.required) { + isValid = false; + validationError = "Broken Pack: Conflict between required groups '" + group.displayName + "' and '" + conflictGroup.displayName + "'"; + return; + } + } + } + } + } + } + + private void initSelections() { + ClientSelectionManager mgr = ClientSelectionManager.getMgr(); + List savedGroups = mgr.getSelectedGroups(content.modpackName); + boolean hasSavedSelection = savedGroups != null && !savedGroups.isEmpty(); + + List savedGroupIds = new ArrayList<>(); + if (hasSavedSelection) { + for (Jsons.ClientSelectionManagerFields.Group g : savedGroups) { + savedGroupIds.add(g.groupId); + } + } + + for (Map.Entry entry : content.groups.entrySet()) { + String id = entry.getKey(); + Jsons.ModpackGroupFields group = entry.getValue(); + + if (isOsCompatible(group.compatibleOS)) { + if (hasSavedSelection) { + if (savedGroupIds.contains(id) || group.required) toggleGroup(id, true); + } else { + if (group.required || group.recommended) toggleGroup(id, true); + } + } + } + } + + public void toggleGroup(String id, boolean state) { + Jsons.ModpackGroupFields group = content.groups.get(id); + if (group == null || !isOsCompatible(group.compatibleOS)) return; + + if (state || group.required) { // Always force state to true if it's required + if (!selectedGroups.contains(id)) { + selectedGroups.add(id); + + // Disable conflicts explicitly broken by THIS group + if (group.breaksWith != null) { + for (String conflict : group.breaksWith) { + if (selectedGroups.contains(conflict)) { + toggleGroup(conflict, false); + } + } + } + + // Disable groups that explicitly break THIS group (Bidirectional conflict) + for (String otherId : new ArrayList<>(selectedGroups)) { + Jsons.ModpackGroupFields other = content.groups.get(otherId); + if (other != null && other.breaksWith != null && other.breaksWith.contains(id)) { + toggleGroup(otherId, false); + } + } + + // Enable requirements recursively + if (group.requires != null) { + for (String req : group.requires) { + if (!selectedGroups.contains(req) && content.groups.containsKey(req)) { + toggleGroup(req, true); + } + } + } + } + } else { + if (selectedGroups.contains(id)) { + selectedGroups.remove(id); + + // Disable dependents recursively (groups that require this group) + for (String otherId : new ArrayList<>(selectedGroups)) { + Jsons.ModpackGroupFields other = content.groups.get(otherId); + if (other != null && other.requires != null && other.requires.contains(id)) { + toggleGroup(otherId, false); + } + } + } + } + } + + private void install() { + ClientSelectionManager mgr = ClientSelectionManager.getMgr(); + if (!mgr.packExists(content.modpackName)) { + mgr.addPack(content.modpackName, new Jsons.ClientSelectionManagerFields.Modpack(new Jsons.ModpackAddresses())); + } + + // Assemble Groups map + List finalGroupsToSave = new ArrayList<>(); + for (String id : selectedGroups) { + finalGroupsToSave.add(new Jsons.ClientSelectionManagerFields.Group(id)); + } + + mgr.setSelectedGroups(content.modpackName, finalGroupsToSave); + mgr.setSelectedPack(content.modpackName); + + Set finalFiles = new HashSet<>(); + for (String id : selectedGroups) { + Jsons.ModpackGroupFields group = content.groups.get(id); + if (group == null || group.files == null) continue; + + finalFiles.addAll(group.files); + } + + // Run update asynchronously so the UI successfully transitions to the download screen without blocking + Util.backgroundExecutor().execute(() -> updater.startUpdate(finalFiles)); + } + + @Override + public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, float delta) { + if (!isValid) { + drawCenteredText(matrices, this.font, VersionedText.literal(validationError).withStyle(ChatFormatting.RED, ChatFormatting.BOLD), this.width / 2, this.height / 2, 0xFF5555); + drawCenteredText(matrices, this.font, VersionedText.literal("Installation Blocked. Please contact the server admin."), this.width / 2, this.height / 2 + 15, 0xAAAAAA); + } else { + if (this.listWidget != null) { + this.listWidget.render(matrices.getContext(), mouseX, mouseY, delta); + } + drawCenteredText(matrices, this.font, this.title.copy(), this.width / 2, 20, 0xFFFFFF); + } + } + + public static boolean isOsCompatible(List compatibleOS) { + if (compatibleOS == null || compatibleOS.isEmpty()) return true; + + String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); + String javaVendor = System.getProperty("java.vendor").toLowerCase(Locale.ROOT); + String javaVmName = System.getProperty("java.vm.name").toLowerCase(Locale.ROOT); + + String currentOs = "UNKNOWN"; + if (osName.contains("win")) currentOs = "WINDOWS"; + else if (osName.contains("mac")) currentOs = "MACOS"; + else if (javaVendor.contains("android") || javaVmName.contains("dalvik") || javaVmName.contains("lemur")) currentOs = "ANDROID"; + else if (osName.contains("linux") || osName.contains("unix")) currentOs = "LINUX"; + + boolean hasIncludes = false; + boolean explicitlyIncluded = false; + boolean explicitlyExcluded = false; + + for (String osReq : compatibleOS) { + String cleanReq = osReq.trim().toUpperCase(Locale.ROOT); + boolean isNegation = cleanReq.startsWith("!"); + String target = isNegation ? cleanReq.substring(1) : cleanReq; + + if (!isNegation) { + hasIncludes = true; + if (currentOs.equals(target)) explicitlyIncluded = true; + } else { + if (currentOs.equals(target)) explicitlyExcluded = true; + } + } + + if (explicitlyExcluded) return false; + if (hasIncludes) return explicitlyIncluded; + return true; // Return true if only negations exist and none matched + } + + class GroupListWidget extends ListEntryWidget { + public GroupListWidget(Minecraft client, int width, int height, int top, int bottom, int itemHeight) { + super(null, client, width, height, top, bottom, itemHeight); + this.clearEntries(); + } + + public void addEntry(GroupEntry entry) { + super.addEntry(entry); + } + + @Override + public int getRowWidth() { + return this.width - 40; // Maximize row width to stretch across the screen + } + + @Override + protected int getScrollbarPosition() { + return this.width - 15; // Push the scrollbar to the far right edge + } + } + + class GroupEntry extends ListEntry { + private final String groupId; + private final Jsons.ModpackGroupFields group; + private int currentX, currentY, currentWidth; + + public GroupEntry(String groupId, Jsons.ModpackGroupFields group) { + super(VersionedText.literal(""), false, ModpackSelectionScreen.this.minecraft); + this.groupId = groupId; + this.group = group; + } + + @Override + public void versionedRender(VersionedMatrices matrices, int x, int y, int entryWidth, int entryHeight) { + this.currentX = x; + this.currentY = y; + this.currentWidth = entryWidth; + + boolean isSelected = selectedGroups.contains(groupId); + boolean isCompat = isOsCompatible(group.compatibleOS); + + String boxText = isSelected ? "[X]" : "[ ]"; + if (!isCompat) boxText = "[-]"; + + int color = isCompat ? (group.required ? 0xAAAAAA : 0xFFFFFF) : 0xFF5555; + + drawText(matrices, minecraft.font, boxText, x + 5, y + 4, color); + drawText(matrices, minecraft.font, group.displayName, x + 25, y + 4, color); + + if (!isCompat) { + drawText(matrices, minecraft.font, VersionedText.literal("Incompatible OS"), x + 25, y + 16, 0xFF5555); + } else if (group.description != null && !group.description.isEmpty()) { + drawText(matrices, minecraft.font, group.description, x + 25, y + 16, 0xAAAAAA); + } + } + + @Override + public Component getNarration() { + return VersionedText.literal(group.displayName + ", " + group.description); + } + + /*? if >= 1.21.9 {*/ + @Override + public boolean mouseClicked(MouseButtonEvent mouseButtonEvent, boolean bl) { + return mouseClickedInternal(mouseButtonEvent.x(), mouseButtonEvent.y(), mouseButtonEvent.button()); + } + /*?} else {*/ + /*@Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return mouseClickedInternal(mouseX, mouseY, button); + }*//*?}*/ + + private boolean mouseClickedInternal(double mouseX, double mouseY, int button) { + if (!isOsCompatible(group.compatibleOS)) return false; + + if (!group.required) { + toggleGroup(groupId, !selectedGroups.contains(groupId)); + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java index 996d9f5a7..a17fbf5cf 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/RestartScreen.java @@ -90,7 +90,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, int lineHeight = 12; // Consistent line spacing // Title - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.restart." + updateType.toString()).withStyle(ChatFormatting.BOLD), this.width / 2, this.height / 2 - 60, @@ -98,7 +98,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Description line 1 - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.restart.description"), @@ -108,7 +108,7 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, ); // Description line 2 - drawCenteredTextWithShadow( + drawCenteredText( matrices, this.font, VersionedText.translatable("automodpack.restart.secDescription"), diff --git a/src/main/java/pl/skidam/automodpack/client/ui/SkipVerificationScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/SkipVerificationScreen.java index a06c6511a..f2ecdbc92 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/SkipVerificationScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/SkipVerificationScreen.java @@ -49,8 +49,6 @@ protected void init() { } public void initWidgets() { - assert this.minecraft != null; - this.textField = new EditBox(this.font, this.width / 2 - 170, this.height / 2 + 15, 340, 20, VersionedText.literal("") ); @@ -59,7 +57,6 @@ public void initWidgets() { this.backButton = buttonWidget(this.width / 2 - 155, this.height / 2 + 80, 150, 20, VersionedText.translatable("automodpack.back"), button -> { - assert this.minecraft != null; this.minecraft.setScreen(verificationScreen); } ); @@ -91,19 +88,15 @@ private void confirmSkip() { if (input.equals(REQUIRED_TEXT)) { confirmButton.active = false; - if (this.minecraft != null) { - this.minecraft.setScreen(parent); - } + this.minecraft.setScreen(parent); validatedCallback.run(); } else { Constants.LOGGER.error("Skip verification text mismatch, try again"); - if (this.minecraft != null) { - /*? if > 1.21.1 {*/ - this.minecraft.getToastManager().addToast(failedToast); - /*?} else {*/ - /*this.minecraft.getToasts().addToast(failedToast); - *//*?}*/ - } + /*? if > 1.21.1 {*/ + this.minecraft.getToastManager().addToast(failedToast); + /*?} else {*/ + /*this.minecraft.getToasts().addToast(failedToast); + *//*?}*/ } } @@ -128,32 +121,32 @@ public void versionedRender(VersionedMatrices matrices, int mouseX, int mouseY, int lineHeight = 12; // Consistent line spacing // Warning title - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.skip.title").withStyle(ChatFormatting.BOLD), this.width / 2, this.height / 2 - 85, TextColors.LIGHT_RED); // Warning message line 1 - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.skip.warning1"), this.width / 2, this.height / 2 - 65, TextColors.WHITE); // Warning message line 2 - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.skip.warning2"), this.width / 2, this.height / 2 - 65 + lineHeight, TextColors.LIGHT_RED); // Instructions - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.skip.instruction"), this.width / 2, this.height / 2 - 35, TextColors.WHITE); // Confirmation prompt - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.translatable("automodpack.validation.skip.confirm.text"), this.width / 2, this.height / 2 - 10, TextColors.WHITE); // Required text to type (displayed prominently) - drawCenteredTextWithShadow(matrices, this.font, + drawCenteredText(matrices, this.font, VersionedText.literal("\"" + REQUIRED_TEXT + "\"").withStyle(ChatFormatting.ITALIC), this.width / 2, this.height / 2 - 10 + lineHeight, TextColors.WHITE); } diff --git a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedMatrices.java b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedMatrices.java index c7ef8d638..e96bac3e5 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedMatrices.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedMatrices.java @@ -45,8 +45,18 @@ public void scale(float x, float y, float z) { } *//*?}*/ /*?} else {*/ - /*public PoseStack getContext() { - return this; + /*private final PoseStack stack; + + public VersionedMatrices(PoseStack stack) { + this.stack = stack; + } + + public VersionedMatrices() { + this.stack = null; + } + + public PoseStack getContext() { + return stack != null ? stack : this; } *//*?}*/ } diff --git a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java index c7087fbe1..55b6540ad 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/versioned/VersionedScreen.java @@ -85,15 +85,39 @@ public void addDrawableChild(T child) { *//*?}*/ /*? if >=1.20 {*/ - public static void drawCenteredTextWithShadow(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int centerX, int y, int color) { + public static void drawCenteredText(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int centerX, int y, int color) { matrices.getContext().drawCenteredString(textRenderer, text, centerX, y, color); } + public static void drawCenteredText(VersionedMatrices matrices, Font textRenderer, String text, int centerX, int y, int color) { + matrices.getContext().drawCenteredString(textRenderer, text, centerX, y, color); + } + // FIXME the centering may be incorrect /*?} else {*/ - /*public static void drawCenteredTextWithShadow(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int centerX, int y, int color) { + /*public static void drawCenteredText(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int centerX, int y, int color) { + textRenderer.drawShadow(matrices.getContext(), text, (float)(centerX - textRenderer.width(text) / 2), (float)y, color); + } + public static void drawCenteredText(VersionedMatrices matrices, Font textRenderer, String text, int centerX, int y, int color) { textRenderer.drawShadow(matrices.getContext(), text, (float)(centerX - textRenderer.width(text) / 2), (float)y, color); } *//*?}*/ + /*? if >=1.20 {*/ + public static void drawText(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int x, int y, int color) { + matrices.getContext().drawString(textRenderer, text, x, y, color); + } + + public static void drawText(VersionedMatrices matrices, Font textRenderer, String text, int x, int y, int color) { + matrices.getContext().drawString(textRenderer, text, x, y, color); + } + /*?} else {*/ + /*public static void drawText(VersionedMatrices matrices, Font textRenderer, MutableComponent text, int x, int y, int color) { + textRenderer.drawShadow(matrices.getContext(), text, x, y, color); + } + + public static void drawText(VersionedMatrices matrices, Font textRenderer, String text, int x, int y, int color) { + textRenderer.drawShadow(matrices.getContext(), text, x, y, color); + } + *//*?}*/ /*? if <1.19.3 {*/ /*public static Button buttonWidget(int x, int y, int width, int height, Component message, Button.OnPress onPress) { @@ -128,7 +152,7 @@ public static void setTooltip(Button button, Component tooltip) { *//*?}*/ /*? if <=1.20 {*/ - /*public static void drawTexture(ResourceLocation textureID, VersionedMatrices matrices, int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) { + /*public static void drawTexture(Identifier textureID, VersionedMatrices matrices, int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) { /^? if <=1.16.5 {^/ /^Minecraft.getInstance().getTextureManager().bindTexture(textureID); ^//^?} else {^/ @@ -141,7 +165,7 @@ public static void drawTexture(Identifier textureID, VersionedMatrices matrices, /*? if >=1.21.6 {*/ matrices.getContext().blit(RenderPipelines.GUI_TEXTURED, textureID, x, y, u, v, width, height, textureWidth, textureHeight); /*?} elif >=1.21.2 {*/ - /*Function RenderTypes = RenderType::guiTextured; + /*Function RenderTypes = RenderType::guiTextured; matrices.getContext().blit(RenderTypes, textureID, x, y, u, v, width, height, textureWidth, textureHeight); *//*?} else {*/ /*matrices.getContext().blit(textureID, x, y, u, v, width, height, textureWidth, textureHeight); diff --git a/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntry.java b/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntry.java index 3b8c1d818..40163f174 100644 --- a/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntry.java +++ b/src/main/java/pl/skidam/automodpack/client/ui/widget/ListEntry.java @@ -84,7 +84,7 @@ public void versionedRender(VersionedMatrices versionedMatrices, int x, int y, i centeredY = centeredY - 10 / 2; } - VersionedScreen.drawCenteredTextWithShadow(versionedMatrices, client.font, text, centeredX, centeredY, TextColors.WHITE); + VersionedScreen.drawCenteredText(versionedMatrices, client.font, text, centeredX, centeredY, TextColors.WHITE); // if (mainPageUrls != null) { // int badgeX = x - 42; diff --git a/src/main/java/pl/skidam/automodpack/init/Common.java b/src/main/java/pl/skidam/automodpack/init/Common.java index 64caa5282..e1554633e 100644 --- a/src/main/java/pl/skidam/automodpack/init/Common.java +++ b/src/main/java/pl/skidam/automodpack/init/Common.java @@ -67,9 +67,9 @@ public static Identifier id(String path) { /*? if >=1.21.11 {*/ return Identifier.tryBuild(MOD_ID, path); /*?} else if >=1.19.2 {*/ - /*return ResourceLocation.tryBuild(MOD_ID, path); + /*return Identifier.tryBuild(MOD_ID, path); *//*?} else {*/ - /*return new ResourceLocation(MOD_ID, path); + /*return new Identifier(MOD_ID, path); *//*?}*/ } } diff --git a/src/main/java/pl/skidam/automodpack/mixin/core/FabricLoginMixin.java b/src/main/java/pl/skidam/automodpack/mixin/core/FabricLoginMixin.java index c842fa8fa..70e305445 100644 --- a/src/main/java/pl/skidam/automodpack/mixin/core/FabricLoginMixin.java +++ b/src/main/java/pl/skidam/automodpack/mixin/core/FabricLoginMixin.java @@ -21,7 +21,7 @@ public class FabricLoginMixin { ) private void dontRemoveAutoModpackChannels(ClientboundCustomQueryPacket packet, CallbackInfo ci) { /*? if <1.20.2 {*/ - /*ResourceLocation id = packet.getIdentifier(); + /*Identifier id = packet.getIdentifier(); *//*?} else {*/ Identifier id = packet.payload().id(); /*?}*/ diff --git a/src/main/java/pl/skidam/automodpack/modpack/Commands.java b/src/main/java/pl/skidam/automodpack/modpack/Commands.java index 4338d9c3a..0743e0d24 100644 --- a/src/main/java/pl/skidam/automodpack/modpack/Commands.java +++ b/src/main/java/pl/skidam/automodpack/modpack/Commands.java @@ -1,8 +1,18 @@ package pl.skidam.automodpack.modpack; +import static net.minecraft.commands.Commands.literal; +import static pl.skidam.automodpack_core.Constants.*; + import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; +import java.util.Set; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Util; /*? if >= 1.21.11 {*/ import net.minecraft.server.permissions.Permission; import net.minecraft.server.permissions.PermissionLevel; @@ -12,79 +22,75 @@ import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.ConfigTools; import pl.skidam.automodpack_core.config.Jsons; -import java.util.Set; -import net.minecraft.ChatFormatting; -import net.minecraft.util.Util; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.network.chat.ClickEvent; -import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.MutableComponent; -import pl.skidam.automodpack_core.config.ConfigUtils; - -import static net.minecraft.commands.Commands.literal; -import static pl.skidam.automodpack_core.Constants.*; public class Commands { public static void register(CommandDispatcher dispatcher) { var automodpackNode = dispatcher.register( - literal("automodpack") - .executes(Commands::about) - .then(literal("generate") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::generateModpack) + literal("automodpack") + .executes(Commands::about) + .then( + literal("generate") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::generateModpack) + ) + .then( + literal("host") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::modpackHostAbout) + .then( + literal("start") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::startModpackHost) ) - .then(literal("host") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::modpackHostAbout) - .then(literal("start") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::startModpackHost) - ) - .then(literal("stop") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::stopModpackHost) - ) - .then(literal("restart") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::restartModpackHost) - ) - .then(literal("connections") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::connections) - ) - .then(literal("fingerprint") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::fingerprint) - ) + .then( + literal("stop") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::stopModpackHost) ) - .then(literal("config") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .then(literal("reload") - .requires((source) -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) - .executes(Commands::reload) - ) + .then( + literal("restart") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::restartModpackHost) ) + .then( + literal("connections") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::connections) + ) + .then( + literal("fingerprint") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::fingerprint) + ) + ) + .then( + literal("config") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .then( + literal("reload") + .requires(source -> source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.byId(3)))) + .executes(Commands::reload) + ) + ) ); - dispatcher.register( - literal("amp") - .executes(Commands::about) - .redirect(automodpackNode) - ); + dispatcher.register(literal("amp").executes(Commands::about).redirect(automodpackNode)); } private static int fingerprint(CommandContext context) { String fingerprint = hostServer.getCertificateFingerprint(); if (fingerprint != null) { - MutableComponent fingerprintText = VersionedText.literal(fingerprint).withStyle(style -> style + MutableComponent fingerprintText = VersionedText.literal(fingerprint).withStyle(style -> + style /*? if >=1.21.5 {*/ .withHoverEvent(new HoverEvent.ShowText(VersionedText.translatable("chat.copy.click"))) - .withClickEvent(new ClickEvent.CopyToClipboard(fingerprint))); - /*?} else {*/ - /*.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, VersionedText.translatable("chat.copy.click"))) + .withClickEvent(new ClickEvent.CopyToClipboard(fingerprint)) + ); + /*?} else {*/ + /*.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, VersionedText.translatable("chat.copy.click"))) .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, fingerprint))); - *//*?}*/ + *//*?}*/ send(context, "Certificate fingerprint", ChatFormatting.WHITE, fingerprintText, ChatFormatting.YELLOW, false); } else { send(context, "Certificate fingerprint is not available. Make sure the server is running with TLS enabled.", ChatFormatting.RED, false); @@ -118,9 +124,8 @@ private static int connections(CommandContext context) { private static int reload(CommandContext context) { Util.backgroundExecutor().execute(() -> { - var tempServerConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV2.class); + var tempServerConfig = ConfigTools.load(serverConfigFile, Jsons.ServerConfigFieldsV3.class); if (tempServerConfig != null) { - ConfigUtils.normalizeServerConfig(tempServerConfig, true); serverConfig = tempServerConfig; send(context, "AutoModpack server config reloaded!", ChatFormatting.GREEN, true); } else { @@ -224,31 +229,22 @@ private static int generateModpack(CommandContext context) { } private static void send(CommandContext context, String msg, ChatFormatting msgColor, boolean broadcast) { - VersionedCommandSource.sendFeedback(context, - VersionedText.literal(msg) - .withStyle(msgColor), - broadcast); + VersionedCommandSource.sendFeedback(context, VersionedText.literal(msg).withStyle(msgColor), broadcast); } private static void send(CommandContext context, String msg, ChatFormatting msgColor, String appendMsg, ChatFormatting appendMsgColor, boolean broadcast) { - VersionedCommandSource.sendFeedback(context, - VersionedText.literal(msg) - .withStyle(msgColor) - .append(VersionedText.literal(" - ") - .withStyle(ChatFormatting.WHITE)) - .append(VersionedText.literal(appendMsg) - .withStyle(appendMsgColor)), - broadcast); + VersionedCommandSource.sendFeedback( + context, + VersionedText.literal(msg).withStyle(msgColor).append(VersionedText.literal(" - ").withStyle(ChatFormatting.WHITE)).append(VersionedText.literal(appendMsg).withStyle(appendMsgColor)), + broadcast + ); } private static void send(CommandContext context, String msg, ChatFormatting msgColor, MutableComponent appendMsg, ChatFormatting appendMsgColor, boolean broadcast) { - VersionedCommandSource.sendFeedback(context, - VersionedText.literal(msg) - .withStyle(msgColor) - .append(VersionedText.literal(" - ") - .withStyle(ChatFormatting.WHITE)) - .append(appendMsg - .withStyle(appendMsgColor)), - broadcast); + VersionedCommandSource.sendFeedback( + context, + VersionedText.literal(msg).withStyle(msgColor).append(VersionedText.literal(" - ").withStyle(ChatFormatting.WHITE)).append(appendMsg.withStyle(appendMsgColor)), + broadcast + ); } } diff --git a/src/main/java/pl/skidam/automodpack/networking/client/LoginResponsePayload.java b/src/main/java/pl/skidam/automodpack/networking/client/LoginResponsePayload.java index 2a200ac7f..82de6292a 100644 --- a/src/main/java/pl/skidam/automodpack/networking/client/LoginResponsePayload.java +++ b/src/main/java/pl/skidam/automodpack/networking/client/LoginResponsePayload.java @@ -4,7 +4,7 @@ import net.minecraft.resources.Identifier; /*? if <1.20.2 {*/ -/*public record LoginResponsePayload(ResourceLocation id, FriendlyByteBuf data) { } +/*public record LoginResponsePayload(Identifier id, FriendlyByteBuf data) { } *//*?} else {*/ import net.minecraft.network.protocol.login.custom.CustomQueryAnswerPayload; import pl.skidam.automodpack.networking.PayloadHelper; diff --git a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java index 4360fee0f..86c46107b 100644 --- a/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java +++ b/src/main/java/pl/skidam/automodpack/networking/packet/DataC2SPacket.java @@ -8,10 +8,12 @@ import pl.skidam.automodpack_core.auth.Secrets; import pl.skidam.automodpack_core.auth.SecretsStore; import pl.skidam.automodpack_core.config.Jsons; +import pl.skidam.automodpack_core.modpack.ClientSelectionManager; import pl.skidam.automodpack_core.utils.AddressHelpers; import pl.skidam.automodpack_loader_core.ReLauncher; import pl.skidam.automodpack_loader_core.client.ModpackUpdater; import pl.skidam.automodpack_loader_core.client.ModpackUtils; +import pl.skidam.automodpack_loader_core.screen.ScreenManager; import pl.skidam.automodpack_loader_core.utils.UpdateType; import java.net.InetSocketAddress; @@ -82,11 +84,14 @@ public static CompletableFuture receive(Minecraft Minecraft, Cl Jsons.ModpackAddresses modpackAddresses = new Jsons.ModpackAddresses(modpackAddress, serverAddress, requiresMagic); var optionalServerModpackContent = ModpackUtils.requestServerModpackContent(modpackAddresses, secret, true); + ClientSelectionManager clientSelectionManager = ClientSelectionManager.getMgr(); + if (optionalServerModpackContent.isPresent()) { ModpackUtils.UpdateCheckResult updateCheckResult = ModpackUtils.isUpdate(optionalServerModpackContent.get(), modpackDir); if (updateCheckResult.requiresUpdate()) { disconnectImmediately(handler); +// new ScreenManager().modpackSelection(optionalServerModpackContent.get()); new ModpackUpdater(optionalServerModpackContent.get(), modpackAddresses, secret, modpackDir).processModpackUpdate(updateCheckResult); needsDisconnecting = true; } else { @@ -99,7 +104,7 @@ public static CompletableFuture receive(Minecraft Minecraft, Cl } if (selectedModpackChanged) { - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + SecretsStore.saveClientSecret(clientSelectionManager.getSelectedPackId(), secret); disconnectImmediately(handler); new ReLauncher(modpackDir, UpdateType.SELECT, null).restart(false); needsDisconnecting = true; @@ -111,8 +116,8 @@ public static CompletableFuture receive(Minecraft Minecraft, Cl needsDisconnecting = true; } - if (clientConfig.selectedModpack != null && !clientConfig.selectedModpack.isBlank()) { - SecretsStore.saveClientSecret(clientConfig.selectedModpack, secret); + if (clientSelectionManager.getSelectedPackId() != null && !clientSelectionManager.getSelectedPackId().isBlank()) { + SecretsStore.saveClientSecret(clientSelectionManager.getSelectedPackId(), secret); } response.writeUtf(String.valueOf(needsDisconnecting), Short.MAX_VALUE); diff --git a/src/main/java/pl/skidam/automodpack/networking/server/LoginRequestPayload.java b/src/main/java/pl/skidam/automodpack/networking/server/LoginRequestPayload.java index ac3e4e41e..ff799af00 100644 --- a/src/main/java/pl/skidam/automodpack/networking/server/LoginRequestPayload.java +++ b/src/main/java/pl/skidam/automodpack/networking/server/LoginRequestPayload.java @@ -3,7 +3,7 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.Identifier; /*? if <1.20.2 {*/ -/*public record LoginRequestPayload(ResourceLocation id, FriendlyByteBuf data) { } +/*public record LoginRequestPayload(Identifier id, FriendlyByteBuf data) { } *//*?} else {*/ import net.minecraft.network.protocol.login.custom.CustomQueryPayload; import pl.skidam.automodpack.networking.PayloadHelper;