diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java index 780c980d..ab4149c0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java @@ -118,10 +118,6 @@ public static void createConfigDialog(EOCVSim eocvSim) { invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); } - public static void createPluginsDialog(EOCVSim eocvSim) { - invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); - } - public static void createAboutDialog(EOCVSim eocvSim) { invokeLater(() -> new About(eocvSim.visualizer.frame, eocvSim)); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index ca37f44e..f28a4b24 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.Visualizer import com.github.serivesmejia.eocvsim.gui.dialog.Output +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.input.SourceType import com.github.serivesmejia.eocvsim.util.FileFilters @@ -102,7 +103,9 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { mFileMenu.add(editSettings) val filePlugins = JMenuItem("Plugins") - filePlugins.addActionListener { DialogFactory.createPluginsDialog(eocvSim) } + filePlugins.addActionListener { eocvSim.pluginManager.appender.append(PluginOutput.SPECIAL_OPEN)} + + mFileMenu.add(filePlugins) mFileMenu.addSeparator() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt index cd4d77df..1bc3ebfc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.swing.Swing import java.awt.FlowLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.awt.event.MouseAdapter import javax.swing.* class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { @@ -81,6 +82,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { sourceSelectorButtonsContainer.add(sourceSelectorCreateBtt) sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt) + sourceSelectorDeleteBtt.isEnabled = false add(sourceSelectorButtonsContainer, GridBagConstraints().apply { gridy = 1 @@ -92,41 +94,46 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { private fun registerListeners() { //listener for changing input sources - sourceSelector.addListSelectionListener { evt -> - if(allowSourceSwitching && !evt.valueIsAdjusting) { - try { - if (sourceSelector.selectedIndex != -1) { - val model = sourceSelector.model - val source = model.getElementAt(sourceSelector.selectedIndex) - - //enable or disable source delete button depending if source is default or not - eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt - .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) - - if (!evt.valueIsAdjusting && source != beforeSelectedSource) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex - } else { - //check if the user requested the pause or if it was due to one shoot analysis when selecting images - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex + sourceSelector.addMouseListener(object: MouseAdapter() { + override fun mouseClicked(e: java.awt.event.MouseEvent) { + val index = (e.source as JList<*>).locationToIndex(e.point) + + if (index != -1) { + if(allowSourceSwitching) { + try { + if (sourceSelector.selectedIndex != -1) { + val model = sourceSelector.model + val source = model.getElementAt(sourceSelector.selectedIndex) + + //enable or disable source delete button depending if source is default or not + sourceSelectorDeleteBtt.isEnabled = eocvSim.inputSourceManager.sources[source]?.isDefault == false + + if (source != beforeSelectedSource) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } else { + //check if the user requested the pause or if it was due to one shoot analysis when selecting images + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } + } } + } else { + sourceSelector.setSelectedIndex(1) } + } catch (ignored: ArrayIndexOutOfBoundsException) { } - } else { - sourceSelector.setSelectedIndex(1) } - } catch (ignored: ArrayIndexOutOfBoundsException) { } } - } + }) // delete selected input source sourceSelectorDeleteBtt.addActionListener { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt index 27b77a83..e6b9d8ec 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt @@ -79,9 +79,22 @@ class PluginOutput( ) : Appendable { companion object { - const val SPECIAL_CLOSE = "[CLOSE]" - const val SPECIAL_CONTINUE = "[CONTINUE]" - const val SPECIAL_SILENT = "[SILENT]" + private const val SPECIAL = "13mck" + + const val SPECIAL_OPEN = "$SPECIAL[OPEN]" + const val SPECIAL_CLOSE = "$SPECIAL[CLOSE]" + const val SPECIAL_CONTINUE = "$SPECIAL[CONTINUE]" + const val SPECIAL_FREE = "$SPECIAL[FREE]" + const val SPECIAL_SILENT = "$SPECIAL[SILENT]" + + fun String.trimSpecials(): String { + return this + .replace(SPECIAL_OPEN, "") + .replace(SPECIAL_CLOSE, "") + .replace(SPECIAL_CONTINUE, "") + .replace(SPECIAL_SILENT, "") + .replace(SPECIAL_FREE, "") + } } private val output = JDialog() @@ -130,6 +143,10 @@ class PluginOutput( private fun handleSpecials(text: String): Boolean { when(text) { + SPECIAL_FREE -> { + mavenBottomButtonsPanel.continueButton.isEnabled = false + mavenBottomButtonsPanel.closeButton.isEnabled = true + } SPECIAL_CLOSE -> close() SPECIAL_CONTINUE -> { mavenBottomButtonsPanel.continueButton.isEnabled = true @@ -137,19 +154,13 @@ class PluginOutput( } } - if(!text.startsWith(SPECIAL_SILENT) && text != SPECIAL_CLOSE) { + if(!text.startsWith(SPECIAL_SILENT) && text != SPECIAL_CLOSE && text != SPECIAL_FREE) { SwingUtilities.invokeLater { output.isVisible = true } } - return text == SPECIAL_CLOSE || text == SPECIAL_CONTINUE - } - - private fun String.trimSpecials(): String { - return this.replace(SPECIAL_CLOSE, "") - .replace(SPECIAL_CONTINUE, "") - .replace(SPECIAL_SILENT, "") + return text == SPECIAL_OPEN || text == SPECIAL_CLOSE || text == SPECIAL_CONTINUE || text == SPECIAL_FREE } override fun append(csq: CharSequence?): java.lang.Appendable? { @@ -201,6 +212,8 @@ class PluginOutput( override fun create(panel: OutputPanel) { layout = BoxLayout(this, BoxLayout.LINE_AXIS) + add(Box.createRigidArea(Dimension(4, 0))) + copyButton.addActionListener { Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(outputTextSupplier()), null) } @@ -221,6 +234,8 @@ class PluginOutput( add(closeButton) closeButton.addActionListener { closeCallback() } + + add(Box.createRigidArea(Dimension(4, 0))) } } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 97c08910..77ac0aae 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -208,6 +208,8 @@ public boolean setInputSource(String sourceName, boolean makeDefault) { public boolean setInputSource(String sourceName) { InputSource src = null; + SysUtil.debugLogCalled("setInputSource"); + if(sourceName == null) { src = new NullSource(); } else { @@ -298,6 +300,8 @@ public void pauseIfImageTwoFrames() { } public void requestSetInputSource(String name) { + SysUtil.debugLogCalled("requestSetInputSource"); + eocvSim.onMainUpdate.doOnce(() -> setInputSource(name)); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index c9a910dd..2547c05a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -34,6 +34,7 @@ import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis @@ -572,7 +573,7 @@ class PipelineManager( logger.info("Changing to pipeline ${pipelineClass.name}") - debugLogCalled("forceChangePipeline") + SysUtil.debugLogCalled("forceChangePipeline") val instantiator = getInstantiatorFor(pipelineClass) @@ -664,7 +665,7 @@ class PipelineManager( } fun requestForceChangePipeline(index: Int) { - debugLogCalled("requestForceChangePipeline") + SysUtil.debugLogCalled("requestForceChangePipeline") onUpdate.doOnce { forceChangePipeline(index) } } @@ -769,15 +770,6 @@ class PipelineManager( forceChangePipeline(0) // default pipeline } - private fun debugLogCalled(name: String) { - val builder = StringBuilder() - for (s in Thread.currentThread().stackTrace) { - builder.appendLine(s.toString()) - } - - logger.debug("$name called in: {}", builder.toString().trim()) - } - } enum class PipelineTimeout(val ms: Long, val coolName: String) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java index 9022214e..fa70650f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java @@ -339,6 +339,16 @@ public static CommandResult runShellCommand(String command) { return result; } + + public static void debugLogCalled(String name) { + StringBuilder builder = new StringBuilder(); + for (StackTraceElement s : Thread.currentThread().getStackTrace()) { + builder.append(s.toString()).append("\n"); + } + + logger.debug("{} called in: {}", name, builder.toString().trim()); + } + public enum OperatingSystem { WINDOWS, LINUX, diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt index 438342d6..940367a3 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt @@ -134,11 +134,13 @@ class PluginLoader( setupFs() - if(pluginToml.contains("min-api-version")) { - val parsedVersion = ParsedVersion(pluginToml.getString("min-api-version")) + if(pluginToml.contains("api-version")) { + val parsedVersion = ParsedVersion(pluginToml.getString("api-version")) if(parsedVersion > EOCVSim.PARSED_VERSION) throw UnsupportedPluginException("Plugin requires a minimum api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + + logger.info("Plugin $pluginName requests min api version of v${parsedVersion}") } if(pluginToml.contains("max-api-version")) { @@ -146,13 +148,17 @@ class PluginLoader( if(parsedVersion < EOCVSim.PARSED_VERSION) throw UnsupportedPluginException("Plugin requires a maximum api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + + logger.info("Plugin $pluginName requests max api version of v${parsedVersion}") } - if(pluginToml.contains("api-version")) { - val parsedVersion = ParsedVersion(pluginToml.getString("api-version")) + if(pluginToml.contains("exact-api-version")) { + val parsedVersion = ParsedVersion(pluginToml.getString("exact-api-version")) + + if(parsedVersion != EOCVSim.PARSED_VERSION) + throw UnsupportedPluginException("Plugin requires an exact api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") - if(parsedVersion == EOCVSim.PARSED_VERSION) - throw UnsupportedPluginException("Plugin request api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + logger.info("Plugin $pluginName requests exact api version of v${parsedVersion}") } if(pluginToml.getBoolean("super-access", false)) { diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt index 58570c5f..8d1d9756 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt @@ -26,6 +26,7 @@ package io.github.deltacv.eocvsim.plugin.loader import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput.Companion.trimSpecials import com.github.serivesmejia.eocvsim.util.JavaProcess import com.github.serivesmejia.eocvsim.util.extension.plus import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder @@ -65,7 +66,11 @@ class PluginManager(val eocvSim: EOCVSim) { appender.subscribe { if(!it.isBlank()) { - logger.info(it) + val message = it.trimSpecials() + + if(message.isNotBlank()) { + logger.info(message) + } } } @@ -97,6 +102,10 @@ class PluginManager(val eocvSim: EOCVSim) { * @see PluginLoader */ fun init() { + eocvSim.visualizer.onInitFinished { + appender.append(PluginOutput.SPECIAL_FREE) + } + repositoryManager.init() val pluginFiles = mutableListOf() diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt index 15b966c2..71973a7f 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt @@ -35,6 +35,7 @@ import io.github.deltacv.eocvsim.plugin.loader.PluginManager import org.jboss.shrinkwrap.resolver.api.maven.ConfigurableMavenResolverSystem import org.jboss.shrinkwrap.resolver.api.maven.Maven import java.io.File +import kotlin.collections.iterator class PluginRepositoryManager( val appender: AppendDelegate, @@ -56,6 +57,8 @@ class PluginRepositoryManager( private lateinit var plugins: Toml private lateinit var cacheToml: Toml + private lateinit var cachePluginsToml: Toml + private lateinit var cacheTransitiveToml: Toml private lateinit var resolver: ConfigurableMavenResolverSystem @@ -65,13 +68,19 @@ class PluginRepositoryManager( val logger by loggerForThis() fun init() { - logger.info("Initializing plugin repository manager") + logger.info("Initializing...") appender // init appender SysUtil.copyFileIs(CACHE_TOML_RES, CACHE_FILE, false) cacheToml = Toml().read(CACHE_FILE) + cachePluginsToml = cacheToml.getTable("plugins") + ?: Toml() + + cacheTransitiveToml = cacheToml.getTable("transitive") + ?: Toml() + SysUtil.copyFileIs(REPOSITORY_TOML_RES, REPOSITORY_FILE, false) pluginsToml = Toml().read(REPOSITORY_FILE) @@ -97,6 +106,7 @@ class PluginRepositoryManager( val files = mutableListOf() val newCache = mutableMapOf() + val newTransitiveCache = mutableMapOf>() var shouldHalt = false @@ -109,62 +119,83 @@ class PluginRepositoryManager( var pluginJar: File? = null try { - var foundCache = false + var isCached = false - for(cached in cacheToml.toMap()) { + mainCacheLoop@ + for(cached in cachePluginsToml.toMap()) { if(cached.key == pluginDep.hexString) { val cachedFile = File(cached.value as String) if(cachedFile.exists()) { + for(transitive in cacheTransitiveToml.getList(pluginDep.hexString) ?: emptyList()) { + val transitiveFile = File(transitive as String) + + if (!transitiveFile.exists()) { + appender.appendln(PluginOutput.SPECIAL_SILENT + "Transitive dependency $transitive for plugin $pluginDep does not exist. Resolving...") + break@mainCacheLoop + } + + _resolvedFiles += transitiveFile // add transitive dependency to resolved files + + newTransitiveCache[pluginDep.hexString] = newTransitiveCache.getOrDefault( + pluginDep.hexString, + mutableListOf() + ).apply { + add(transitiveFile.absolutePath) + } + } + appender.appendln( PluginOutput.SPECIAL_SILENT + - "Found cached plugin dependency $pluginDep (${pluginDep.hexString})" + "Found cached plugin \"$pluginDep\" (${pluginDep.hexString}). All transitive dependencies OK." ) pluginJar = cachedFile _resolvedFiles += cachedFile - foundCache = true - break - } else { - newCache.remove(cached.key) - } - } - } - if(foundCache) { - appender.appendln(PluginOutput.SPECIAL_SILENT + "Resolving plugin ${plugin.key} at ${plugin.value}") - } else { - appender.appendln("Resolving plugin ${plugin.key} at ${plugin.value}") - } + newCache[pluginDep.hexString] = cachedFile.absolutePath // add plugin to revalidated cache - resolver.resolve(pluginDep) - .withTransitivity() - .asFile() - .forEach { file -> - if(pluginJar == null) { - // the first file is the plugin jar - pluginJar = file - newCache[pluginDep.hexString] = pluginJar!!.absolutePath + isCached = true // skip to next plugin, this one is already resolved by cache } - _resolvedFiles += file + break@mainCacheLoop } + } + + if(!isCached) { + // if we reach this point, the plugin was not found in cache + appender.appendln("Resolving plugin ${plugin.key} at \"${plugin.value}\"...") + + resolver.resolve(pluginDep) + .withTransitivity() + .asFile() + .forEach { file -> + if (pluginJar == null) { + // the first file returned by maven is the plugin jar we want + pluginJar = file + newCache[pluginDep.hexString] = pluginJar!!.absolutePath + } else { + newTransitiveCache[pluginDep.hexString] = newTransitiveCache.getOrDefault( + pluginDep.hexString, + mutableListOf() + ).apply { + add(file.absolutePath) + } // add transitive dependency to cache + } + + _resolvedFiles += file // add file to resolved files to later build a classpath + } + } files += pluginJar!! } catch(ex: Exception) { - logger.warn("Failed to resolve plugin dependency $pluginDep", ex) + logger.warn("Failed to resolve plugin dependency \"$pluginDep\"", ex) appender.appendln("Failed to resolve plugin ${plugin.key}: ${ex.message}") shouldHalt = true } } - val cacheBuilder = StringBuilder() - cacheBuilder.append("# Do not edit this file, it is generated by the application.\n") - for(cached in newCache) { - cacheBuilder.append("${cached.key} = \"${cached.value.replace("\\", "/")}\"\n") - } - - SysUtil.saveFileStr(CACHE_FILE, cacheBuilder.toString()) + writeCacheFile(newCache, newTransitiveCache) if(shouldHalt) { appender.append(PluginOutput.SPECIAL_CONTINUE) @@ -177,6 +208,38 @@ class PluginRepositoryManager( return files } + + private fun writeCacheFile( + cache: Map, + transitiveCache: Map> + ) { + val cacheBuilder = StringBuilder() + + cacheBuilder.appendLine("# Do not edit this file, it is generated by the application.") + + cacheBuilder.appendLine("[plugins]") // add plugins table + + for(cached in cache) { + cacheBuilder.appendLine("${cached.key} = \"${cached.value.replace("\\", "/")}\"") + } + + cacheBuilder.appendLine("[transitive]") // add transitive dependencies table + + for((plugin, deps) in transitiveCache) { + cacheBuilder.appendLine("$plugin = [") // add plugin hash as key + + for((i, dep) in deps.withIndex()) { + cacheBuilder.append("\t\"${dep.replace("\\", "/")}\"") // add dependency path + if(i < deps.size - 1) cacheBuilder.append(",") // add comma if not last + + cacheBuilder.appendLine() + } + + cacheBuilder.appendLine("]") + } + + SysUtil.saveFileStr(CACHE_FILE, cacheBuilder.toString().trim()) + } }