[1.16+] Экстрактор файлов из Jar мода

88
4
6
Не знаю как у вас, но у меня утро начинается с модинга в 9 утра 1691981501686.png, поэтому я решил написать тему про эктрактор файлов.
Вообще, это не совсем моддинг, по большей части стандартная Java, но на Stackoveflow я нормального решения не нашел, а те что находил - не работают, поэтому ловите гайдик.
Начнем с основ. Прочитайте данное сообщение, потом уже продолжайте:

ДАННЫЙ КОД ЯВЛЯЕТСЯ СТАНДАРТНОЙ JAVA(и Kotlin).
ЕСЛИ ВЫ НЕ ЗНАЕТЕ JAVA/KOTLIN, НЕ ЛЕЗТЕ СЮДА.
НЕ РАБОТАЕТ НА MCREATOR!

Итак,продолжим.
Я начну с Kotlin, поэтому, если вам нужна Java, листайте ниже.
Для начала создадим класс JarExtractor, после загружаем в него следующее содержимое

JarExtractor.kt:
class JarExtractor{
    fun relocate() {
        if (FMLEnvironment.production) {
            /**
            * папка, откуда доставаться файлы
            *
            * Выглядит это примерно так:
            * 
            * jar://directoryoffiles/MyDir 
            */ 
            extractScripts("MyDir") 
        } else {
            // Сообщение о том, что невозможно распоковать файлы, если у нас FML не в продукте, а в ide
            Constant.LOGGER.warn("Failed to automatic extract files!")
            Constant.LOGGER.warn("If this is developerment side, you can ignore this message")
            Constant.LOGGER.warn("You will have to manually copy the scripts located on the path: mymodid/*, where * is the name of the directory.")
            Constant.LOGGER.warn("Else, send this to developer")
        }
    }

    private fun extract(sourceFolder: String) {
        val resource = "directoryoffiles/${sourceFolder}" // директория в нашем jar файле
        val path = FMLPaths.GAMEDIR.get().resolve("mydir/").resolve("myfiles/") // путь до того, где будут наши файлы
        val exitFolder= path.toFile().path
        extract(resource, exitFolder)
    }

    private fun extract(sourceFolder: String, targetFolder: String) {
        val jarFilePath = ModList.get().getModFileById("mymodid").file.filePath.toFile() // Получаем  Jar'ник через ModId

        try {
            val jarFile = JarFile(jarFilePath)
            jarFile.stream()
                .filter { entry -> entry.name.startsWith(sourceFolder) }
                .forEach { entry -> saveFileFromJar(entry, targetFolder) }
            Constant.LOGGER.debug("Extraction completed successfully!")
            jarFile.close()
        } catch (e: IOException) {
            LOGGER.error("Extraction failed!", e)
        }
    }
    // Сам сейвер
    private fun saveFileFromJar(entry: JarEntry, targetFolder: String) {
        try {
            val output = File(targetFolder, entry.name)
            if (entry.isDirectory) Files.createDirectories(output.toPath())
            else {
                JarExtractor::class.java.getResourceAsStream("/" + entry.name).use { inputStream ->
                    FileOutputStream(output).use { outputStream ->
                        val buffer = ByteArray(4096)
                        var bytesRead: Int
                        if (inputStream != null) {
                            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                                outputStream.write(buffer, 0, bytesRead)
                            }
                        }
                    }
                }
            }
        } catch (e: IOException) {
            LOGGER.error("Saving Failed!", e)
        }
    }

    companion object {
        @JvmStatic
        private var instance: JarExtractor? = null // Нулляем переменную, чтобы потом наделить ее значением

        @JvmStatic
        fun getInstance(): JarScriptExtractor {
            if (instance == null) instance = JarExtractor() // При первом старте мода, будет стартовать и инстанс, без повторного вызова new
            return instance!!
        }
    }
}

В результате, у нас получится, что в папку с майном, по пути mydir/myfiles/mymodid/directoryoffiles/MyDir будут извлекаться файлы из нашего Jarnika.

а теперь то же самое, только на java
JarExtractor.java:
class JarExtractor {
    public static void relocate() {
        extractScripts("MyDir");
    }

    public static void extract(String sourceFolderPath) {
        String resourcePath = "directoryoffiles/" + sourceFolderPath;
        Path path = FMLPaths.GAMEDIR.get().resolve("mydir/").resolve("myfiles/");
        String hesp = path.toFile().getPath();

        extract(resourcePath, hesp);
    }

    public static void extract(String sourceFolderPath, String targetFolder) {
        File jarFilePath = ModList.get().getModFileById(Constant.ModId).getFile().getFilePath().toFile();

        try {
            JarFile jarFile = new JarFile(jarFilePath);
            jarFile.stream()
                    .filter(entry -> entry.getName().startsWith(sourceFolderPath))
                    .forEach(entry -> saveFileFromJar(entry, targetFolder));
            jarFile.close();

            Constant.LOGGER.debug("Extraction completed successfully!");
        } catch (IOException e) {
            Constant.LOGGER.error("Extraction failed!", e);
        }
    }

    private static void saveFileFromJar(JarEntry entry, String targetFolderPath) {
        try {
            File outputFile = new File(targetFolderPath, entry.getName());
            if (entry.isDirectory()) {
                Files.createDirectories(outputFile.toPath());
            } else {
                try (InputStream inputStream = JarExtractor.class.getResourceAsStream("/" + entry.getName());
                     FileOutputStream outputStream = new FileOutputStream(outputFile)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                }
            }
        } catch (IOException e) {
            Constant.LOGGER.error("Saving Failed!", e);
        }
    }
}

Итак, на этом все! Если хотите, можете поиграться с этим и сделать возможность кастома своего мода (если, конечно, с ассетридером разберетесь)

P.s. Снимаю с себя ответственность этим сообщением, если вдруг, кто-то решит сделать малварь, основываясь на этом коде.

P.p.s. Я код не смог найти ни в KubeJS, ни в CraftTweaker зато, нашел не понятную лапшу

Кто там говорил, что разработка на Java не мое и мне надо завязывать? Ну.. Считайте так дальше..
1691983640999.png
 
1,371
112
241
А теперь самый главный вопрос: а зачем?
Можно же использовать любой архиватор для открытия jar-ников, соответственно, и любой ассет тоже можно достать. И работать должно не хуже вашего.
Я бы понял, если бы оно декомпилило классы и приводило бы их в читабельный вид - это полезно. Но просто вытащить ассеты с мода можно и ручками, испытывая при том 0 проблем.

Итог: программка может и прикольная, но абсолютно бесполезная.
 
88
4
6
Можно же использовать любой архиватор для открытия jar-ников, соответственно, и любой ассет тоже можно достать.
Да можно, но а если это идет куда-нибудь в публичный доступ? Всем архиватором доставать файлы?
Я это написал, чтобы как раз ручками ничего не делать
 
1,371
112
241
Да можно, но а если это идет куда-нибудь в публичный доступ?
Не понял. Ассеты же защищены (обычно) в публичном доступе под АП, не?
Всем архиватором доставать файлы?
Скинул папку на раб. стол и готово. Один в один то же, что делает программка.
 
434
41
110
сделать возможность кастома своего мода
Хочешь полную кастомизацию? Распланируй архитектуру мода так, чтобы все нужные элементы хранились как Json-подобном документе, и считывались при загрузке.

Взял например Tson Конфигурации, которые позволяют записать структуру обьектов как обычную строку, написал например функцию, которая будет преобразовывать эту структуру в нужный блок со всеми параметрами. А теперь к черту Java, если нужен новый блок - пишу на Tson. + Любой школьник сможет открыть этот Tson и накастомизировать под себя

Да можно, но а если это идет куда-нибудь в публичный доступ? Всем архиватором доставать файлы?
Я это написал, чтобы как раз ручками ничего не делать
Все еще не могу представить реальную рабочую задачу, где это пригодиться

Всем архиватором доставать файлы?
Прошу заменить алгоритмы сжатия deflate/deflate 64 поддерживаются стандартным проводником windows, тупо переименовал в zip и открываешь
 
1,371
112
241
@will0376 Но всё это же самое можно сделать обычным архиватором (7-Zip/WinRar). Даже стандартным Windows'ским. Не вижу смысла.
 
1,074
72
372
Для начала создадим класс JarExtractor, после загружаем в него следующее содержимое
Советую подтянуть свои знания по Java/Kotlin. Код уровня древней Java 6. Столько возможностей чайчас появилось чтобы написать его лучше...
 

will0376

Токсичная личность
2,079
55
585
Но всё это же самое можно сделать обычным архиватором (7-Zip/WinRar)
т.е. ты предлагаешь заставить пользователей распаковать, скажем дефолтные шиматики для мода, самостоятельно?
Про то, что их можно использовать напрямую из архива - пока не рассматриваем.
 
1,371
112
241
т.е. ты предлагаешь заставить пользователей распаковать, скажем дефолтные шиматики для мода, самостоятельно?
Код выше явно не для пользовательского использования (т.к. типичный пользователь вряд ли напишет небольшую подстраиваемую программу и скомпилирует её). К тому же, большинство дефолтных schematic'ов либо уже встроены в моды, либо в свободном доступе.
 
88
4
6
434
41
110
Я как мододел вижу тут возможность реализации распаковки статических ресурсов...
Еще раз спрошу, зачем распаковывать то? Что IO-стрим из jar запускать - что из файла - не велика разница. Хотя нет, разница есть, на диске мертвым грузом лежит копия того, что уже упаковано в jar
 

will0376

Токсичная личность
2,079
55
585
Еще раз спрошу, зачем распаковывать то? Что IO-стрим из jar запускать - что из файла - не велика разница. Хотя нет, разница есть, на диске мертвым грузом лежит копия того, что уже упаковано в jar
А я еще раз напишу:
Про то, что их можно использовать напрямую из архива - пока не рассматриваем.

Почти любой код бывает полезен.
 
1,371
112
241
Причем тут пользователь?
т.е. ты предлагаешь заставить пользователей распаковать
Ты сам начал говорить о пользователях. Я тебе ответил же
Код выше явно не для пользовательского использования (т.к. типичный пользователь вряд ли напишет небольшую подстраиваемую программу и скомпилирует её).


Я как мододел вижу тут возможность реализации распаковки статических ресурсов...
Ты про распаковку ассетов из jar-ника/zip-ки/dat-ника (и т.д.) для условных картин, звуков и пр.? Но их проще напрямую скачать, не?
Почти любой код бывает полезен.
Ключевое слово почти. И к сожалению, на мой взгляд, это не про код в данной теме.
 
1,074
72
372
Что IO-стрим из jar запускать - что из файла - не велика разница. Хотя нет, разница есть, на диске мертвым грузом лежит копия того, что уже упаковано в jar
И не только. Из архива операции с мелкими файлами выполняются быстрее. Сравните скорость загрузки текстур из архива и в распакованном виде.
 
1,111
47
420
Я как то покекал с твоего повсеместного флекса этим кодом. Мне было скучно и я сделал ревью. В целом он, по идее, работает, но определенно не является примером для подражания.
Всегда ваш интернет-воин.

Kotlin:
class JarExtractor{
    fun relocate() {
        if (FMLEnvironment.production) {
            /**
             * папка, откуда доставаться файлы
             *
             * Выглядит это примерно так:
             *
             * jar://directoryoffiles/MyDir
             */
            extractScripts("MyDir")
        } else {
            // NOTE: What is stopping you from using files from resources?
            // Сообщение о том, что невозможно распоковать файлы, если у нас FML не в продукте, а в ide
            Constant.LOGGER.warn("Failed to automatic extract files!")
            Constant.LOGGER.warn("If this is developerment side, you can ignore this message")
            Constant.LOGGER.warn("You will have to manually copy the scripts located on the path: mymodid/*, where * is the name of the directory.")
            Constant.LOGGER.warn("Else, send this to developer")
        }
    }

    private fun extract(sourceFolder: String) {
        val resource = "directoryoffiles/${sourceFolder}" // директория в нашем jar файле
        // NOTE: The whole point of resolve methods is to avoid using path separators and string concatenation.
        val path = FMLPaths.GAMEDIR.get().resolve("mydir/").resolve("myfiles/") // путь до того, где будут наши файлы
        
        // NOTE: Why would you transform File to String here? Just to reverse it some lines later?
        val exitFolder= path.toFile().path
        extract(resource, exitFolder)
    }

    private fun extract(sourceFolder: String, targetFolder: String) {
        val jarFilePath: File = ModList.get().getModFileById("mymodid").file.filePath.toFile() // Получаем  Jar'ник через ModId

        try {
            val jarFile = JarFile(jarFilePath)
            jarFile.stream()
                .filter { entry -> entry.name.startsWith(sourceFolder) }
                .forEach { entry -> saveFileFromJar(entry, targetFolder) }
            Constant.LOGGER.debug("Extraction completed successfully!")
            jarFile.close() // NOTE: try-with-resources
        } catch (e: IOException) {
            // NOTE: clenup?
            LOGGER.error("Extraction failed!", e)
        }
    }
    // Сам сейвер
    private fun saveFileFromJar(entry: JarEntry, targetFolder: String) {
        try {
            // NOTE: Why not use Path.resolve()?
            // NOTE: Funny thing to know. From this point your code becomes platform-specific as it relates to fact that system uses / as path separator. 
            val output = File(targetFolder, entry.name)
            if (entry.isDirectory) Files.createDirectories(output.toPath()) // NOTE: Once again mixing Path and File API.
            else {
                JarExtractor::class.java.getResourceAsStream("/" + entry.name).use { inputStream ->
                    // NOTE: Why not use ByteStreams.copy()?
                    FileOutputStream(output).use { outputStream ->
                        val buffer = ByteArray(4096)
                        var bytesRead: Int
                        if (inputStream != null) {
                            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                                outputStream.write(buffer, 0, bytesRead)
                            }
                        }
                    }
                }
            }
        } catch (e: IOException) {
            LOGGER.error("Saving Failed!", e)
            // NOTE: should be thrown.
        }
    }

    companion object {
        // NOTE: by lazy?
        // NOTE: Why not using singleton with static init block?
        @JvmStatic
        private var instance: JarExtractor? = null // Нулляем переменную, чтобы потом наделить ее значением

        @JvmStatic
        fun getInstance(): JarScriptExtractor {
            if (instance == null) instance = JarExtractor() // При первом старте мода, будет стартовать и инстанс, без повторного вызова new
            return instance!!
        }
    }
}
 
Сверху