commit c3f56051071267b8cf5e9696d9d6af90e19dd07e Author: Finn Date: Wed Jan 14 15:15:02 2026 +0100 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fac4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..cc20609 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Hytale \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..ce1c62c --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..88cf87c --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..44fc632 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/coroutines-javaagent.jar b/.intellijPlatform/coroutines-javaagent.jar new file mode 100644 index 0000000..fe21a24 Binary files /dev/null and b/.intellijPlatform/coroutines-javaagent.jar differ diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ai.grazie.spell.gec.engine.local-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ai.grazie.spell.gec.engine.local-IU-252.27397.103.xml new file mode 100644 index 0000000..d689fe4 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ai.grazie.spell.gec.engine.local-IU-252.27397.103.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.compose.foundation.desktop-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.compose.foundation.desktop-IU-252.27397.103.xml new file mode 100644 index 0000000..52a5d83 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.compose.foundation.desktop-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.kotlinx.io-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.kotlinx.io-IU-252.27397.103.xml new file mode 100644 index 0000000..65f242f --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.kotlinx.io-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ktor.client-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ktor.client-IU-252.27397.103.xml new file mode 100644 index 0000000..844c981 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.ktor.client-IU-252.27397.103.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.lucene.common-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.lucene.common-IU-252.27397.103.xml new file mode 100644 index 0000000..55d97d0 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.lucene.common-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.microba-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.microba-IU-252.27397.103.xml new file mode 100644 index 0000000..baf0ffa --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.microba-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.skiko-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.skiko-IU-252.27397.103.xml new file mode 100644 index 0000000..150aac5 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.libraries.skiko-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.backend-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.backend-IU-252.27397.103.xml new file mode 100644 index 0000000..223ef6a --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.backend-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.compose-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.compose-IU-252.27397.103.xml new file mode 100644 index 0000000..7bc8de1 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.compose-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.foundation-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.foundation-IU-252.27397.103.xml new file mode 100644 index 0000000..60c522e --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.foundation-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ideLafBridge-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ideLafBridge-IU-252.27397.103.xml new file mode 100644 index 0000000..945cf6d --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ideLafBridge-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ui-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ui-IU-252.27397.103.xml new file mode 100644 index 0000000..bb7f917 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.jewel.ui-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl-IU-252.27397.103.xml new file mode 100644 index 0000000..31876f9 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl-IU-252.27397.103.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.lang-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.lang-IU-252.27397.103.xml new file mode 100644 index 0000000..619abec --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.lang-IU-252.27397.103.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.shared-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.shared-IU-252.27397.103.xml new file mode 100644 index 0000000..4bf2170 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.platform.vcs.impl.shared-IU-252.27397.103.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend-IU-252.27397.103.xml new file mode 100644 index 0000000..4cf1abc --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend-IU-252.27397.103.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend.psi-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend.psi-IU-252.27397.103.xml new file mode 100644 index 0000000..58e71ba --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.properties.backend.psi-IU-252.27397.103.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.spellchecker-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.spellchecker-IU-252.27397.103.xml new file mode 100644 index 0000000..765638f --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledModule-intellij.spellchecker-IU-252.27397.103.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.java-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.java-IU-252.27397.103.xml new file mode 100644 index 0000000..3475d59 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.java-IU-252.27397.103.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.modules.json-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.modules.json-IU-252.27397.103.xml new file mode 100644 index 0000000..396a8cc --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-com.intellij.modules.json-IU-252.27397.103.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.maven-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.maven-IU-252.27397.103.xml new file mode 100644 index 0000000..0eb8732 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.maven-IU-252.27397.103.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.reposearch-IU-252.27397.103.xml b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.reposearch-IU-252.27397.103.xml new file mode 100644 index 0000000..6f46fc3 --- /dev/null +++ b/.intellijPlatform/localPlatformArtifacts/IU-252.27397.103/bundledPlugin-org.jetbrains.idea.reposearch-IU-252.27397.103.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.intellijPlatform/self-update.lock b/.intellijPlatform/self-update.lock new file mode 100644 index 0000000..1e9270b --- /dev/null +++ b/.intellijPlatform/self-update.lock @@ -0,0 +1 @@ +2026-01-13 \ No newline at end of file diff --git a/.run/Run IDE with Plugin.run.xml b/.run/Run IDE with Plugin.run.xml new file mode 100644 index 0000000..196652f --- /dev/null +++ b/.run/Run IDE with Plugin.run.xml @@ -0,0 +1,23 @@ + + + + + + + + true + false + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64fa696 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 UnlegitDqrk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..112e56b --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# HytaleWizardIntelliJ + +Hytale plugin for the IntelliJ Wizard UI + +# IntelliJ Platform Plugin Template + +[![Twitter Follow](https://img.shields.io/badge/follow-%40JBPlatform-1DA1F2?logo=twitter)](https://twitter.com/JBPlatform) +[![Developers Forum](https://img.shields.io/badge/JetBrains%20Platform-Join-blue)][jb:forum] + +## Plugin template structure + +A generated project contains the following content structure: + +``` +. +├── .run/ Predefined Run/Debug Configurations +├── build/ Output build directory +├── gradle +│ ├── wrapper/ Gradle Wrapper +├── src Plugin sources +│ ├── main +│ │ ├── kotlin/ Kotlin production sources +│ │ └── resources/ Resources - plugin.xml, icons, messages +├── .gitignore Git ignoring rules +├── build.gradle.kts Gradle build configuration +├── gradle.properties Gradle configuration properties +├── gradlew *nix Gradle Wrapper script +├── gradlew.bat Windows Gradle Wrapper script +├── README.md README +└── settings.gradle.kts Gradle project settings +``` + +In addition to the configuration files, the most crucial part is the `src` directory, which contains our implementation +and the manifest for our plugin – [plugin.xml][file:plugin.xml]. + +> [!NOTE] +> To use Java in your plugin, create the `/src/main/java` directory. + +## Plugin configuration file + +The plugin configuration file is a [plugin.xml][file:plugin.xml] file located in the `src/main/resources/META-INF` +directory. +It provides general information about the plugin, its dependencies, extensions, and listeners. + +You can read more about this file in the [Plugin Configuration File][docs:plugin.xml] section of our documentation. + +If you're still not quite sure what this is all about, read our +introduction: [What is the IntelliJ Platform?][docs:intro] + +$H$H Predefined Run/Debug configurations + +Within the default project structure, there is a `.run` directory provided containing predefined *Run/Debug +configurations* that expose corresponding Gradle tasks: + +| Configuration name | Description | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Run Plugin | Runs [`:runIde`][gh:intellij-platform-gradle-plugin-runIde] IntelliJ Platform Gradle Plugin task. Use the *Debug* icon for plugin debugging. | +| Run Tests | Runs [`:test`][gradle:lifecycle-tasks] Gradle task. | +| Run Verifications | Runs [`:verifyPlugin`][gh:intellij-platform-gradle-plugin-verifyPlugin] IntelliJ Platform Gradle Plugin task to check the plugin compatibility against the specified IntelliJ IDEs. | + +> [!NOTE] +> You can find the logs from the running task in the `idea.log` tab. + +## Publishing the plugin + +> [!TIP] +> Make sure to follow all guidelines listed in [Publishing a Plugin][docs:publishing] to follow all recommended and +> required steps. + +Releasing a plugin to [JetBrains Marketplace](https://plugins.jetbrains.com) is a straightforward operation that uses +the `publishPlugin` Gradle task provided by +the [intellij-platform-gradle-plugin][gh:intellij-platform-gradle-plugin-docs]. + +You can also upload the plugin to the [JetBrains Plugin Repository](https://plugins.jetbrains.com/plugin/upload) +manually via UI. + +## Useful links + +- [IntelliJ Platform SDK Plugin SDK][docs] +- [IntelliJ Platform Gradle Plugin Documentation][gh:intellij-platform-gradle-plugin-docs] +- [IntelliJ Platform Explorer][jb:ipe] +- [JetBrains Marketplace Quality Guidelines][jb:quality-guidelines] +- [IntelliJ Platform UI Guidelines][jb:ui-guidelines] +- [JetBrains Marketplace Paid Plugins][jb:paid-plugins] +- [IntelliJ SDK Code Samples][gh:code-samples] + +[docs]: https://plugins.jetbrains.com/docs/intellij + +[docs:intro]: https://plugins.jetbrains.com/docs/intellij/intellij-platform.html?from=IJPluginTemplate + +[docs:plugin.xml]: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html?from=IJPluginTemplate + +[docs:publishing]: https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate + +[file:plugin.xml]: ./src/main/resources/META-INF/plugin.xml + +[gh:code-samples]: https://github.com/JetBrains/intellij-sdk-code-samples + +[gh:intellij-platform-gradle-plugin]: https://github.com/JetBrains/intellij-platform-gradle-plugin + +[gh:intellij-platform-gradle-plugin-docs]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html + +[gh:intellij-platform-gradle-plugin-runIde]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-tasks.html#runIde + +[gh:intellij-platform-gradle-plugin-verifyPlugin]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-tasks.html#verifyPlugin + +[gradle:lifecycle-tasks]: https://docs.gradle.org/current/userguide/java_plugin.html#lifecycle_tasks + +[jb:github]: https://github.com/JetBrains/.github/blob/main/profile/README.md + +[jb:forum]: https://platform.jetbrains.com/ + +[jb:quality-guidelines]: https://plugins.jetbrains.com/docs/marketplace/quality-guidelines.html + +[jb:paid-plugins]: https://plugins.jetbrains.com/docs/marketplace/paid-plugins-marketplace.html + +[jb:quality-guidelines]: https://plugins.jetbrains.com/docs/marketplace/quality-guidelines.html + +[jb:ipe]: https://jb.gg/ipe + +[jb:ui-guidelines]: https://jetbrains.github.io/ui \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..446e2db --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("java") + id("org.jetbrains.intellij.platform") version "2.10.2" +} + +group = "dev.unlegitdqrk" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + intellijPlatform { + defaultRepositories() + } +} + +// Dependencies for the plugin +dependencies { + intellijPlatform { + intellijIdea("2025.2.4") + testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Platform) + + // Bundled Plugins for the project (if necessary) + bundledPlugin("com.intellij.java") + bundledPlugin("com.intellij.modules.json") + bundledPlugin("org.jetbrains.idea.maven") + } +} + +intellijPlatform { + pluginConfiguration { + ideaVersion { + sinceBuild = "252.25557" + } + changeNotes = """ + Initial version + """.trimIndent() + } +} + +tasks { + // Set JVM compatibility versions for Java 21 + withType { + sourceCompatibility = "21" + targetCompatibility = "21" + options.encoding = "UTF-8" + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e679d33 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache=true +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true +org.jetbrains.intellij.platform.selfUpdateCheck=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d30212c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..854d93e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +rootProject.name = "Hytale" + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} \ No newline at end of file diff --git a/src/main/java/dev/unlegitdqrk/wizard/CustomProjectModuleBuilder.java b/src/main/java/dev/unlegitdqrk/wizard/CustomProjectModuleBuilder.java new file mode 100644 index 0000000..c978dbe --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/CustomProjectModuleBuilder.java @@ -0,0 +1,543 @@ +package dev.unlegitdqrk.wizard; + +import com.intellij.ide.projectView.ProjectView; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.ide.util.projectWizard.ModuleBuilder; +import com.intellij.ide.util.projectWizard.ModuleWizardStep; +import com.intellij.ide.util.projectWizard.WizardContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.module.ModifiableModuleModel; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleType; +import com.intellij.openapi.module.ModuleTypeManager; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ContentEntry; +import com.intellij.openapi.roots.ModifiableRootModel; +import com.intellij.openapi.roots.ui.configuration.ModulesProvider; +import com.intellij.openapi.startup.StartupManager; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.progress.ProcessCanceledException; +import dev.unlegitdqrk.wizard.table.AuthorRow; +import dev.unlegitdqrk.wizard.table.KeyValueRow; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.model.java.JavaResourceRootType; +import org.jetbrains.jps.model.java.JavaSourceRootType; +import org.jetbrains.idea.maven.project.MavenProjectsManager; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * ModuleBuilder that generates a Hytale JavaPlugin project (Maven only) with manifest.json. + */ +public final class CustomProjectModuleBuilder extends ModuleBuilder { + private Sdk selectedJdk; + + private String groupId; + private String artifactId; + private String projectVersion; + + private String javaPackage; + private String mainClassName; + + private String hytaleServerVersion; + + // Manifest fields (exact casing as provided) + private String manifestGroup; + private String manifestName; + private String manifestVersion; + private String manifestDescription; + private String manifestWebsite; + private String manifestServerVersion; + + private List authors = List.of(); + private List dependencies = List.of(); + private List optionalDependencies = List.of(); + private List loadBefore = List.of(); + + private boolean manifestDisabledByDefault; + private boolean manifestIncludesAssetPack; + + @Override + public @NotNull ModuleType getModuleType() { + ModuleType type = ModuleTypeManager.getInstance().findByID(HytaleModuleType.ID); + return type != null ? type : new HytaleModuleType(); + } + + @Override + public void setupRootModel(@NotNull ModifiableRootModel model) throws ConfigurationException { + String contentPath = getContentEntryPath(); + if (contentPath == null || contentPath.isBlank()) { + throw new ConfigurationException("Content entry path is empty."); + } + + VirtualFile rootDir = createAndGetRootDir(contentPath); + model.addContentEntry(rootDir); + + // Create Maven standard layout only (do NOT mark roots here) + VirtualFile srcMainJava = mkdirs(rootDir, "src/main/java"); + VirtualFile srcMainRes = mkdirs(rootDir, "src/main/resources"); + VirtualFile srcTestJava = mkdirs(rootDir, "src/test/java"); + VirtualFile srcTestRes = mkdirs(rootDir, "src/test/resources"); + + generateBuildFiles(rootDir); // creates pom.xml + generateSourcesAndResources(srcMainJava, srcMainRes); + + // SDK handling + if (selectedJdk != null && com.intellij.openapi.projectRoots.ProjectJdkTable.getInstance().findJdk(selectedJdk.getName()) != null) + model.setSdk(selectedJdk); + else model.inheritSdk(); + } + + @Override + public ModuleWizardStep @NotNull [] createWizardSteps(@NotNull WizardContext wizardContext, + @NotNull ModulesProvider modulesProvider) { + if (isBlank(artifactId) && wizardContext.getProjectName() != null) { + artifactId = wizardContext.getProjectName(); + } + if (isBlank(manifestName) && wizardContext.getProjectName() != null) { + manifestName = wizardContext.getProjectName(); + } + if (isBlank(mainClassName)) { + mainClassName = "MyPlugin"; + } + return new ModuleWizardStep[]{new CustomProjectWizardStep(this)}; + } + + private void generateBuildFiles(VirtualFile rootDir) throws ConfigurationException { + requireNonBlank(groupId, "groupId"); + requireNonBlank(artifactId, "artifactId"); + requireNonBlank(projectVersion, "version"); + requireNonBlank(hytaleServerVersion, "Hytale Server Version"); + + String pomVersion = hytaleServerVersion.equalsIgnoreCase("?") || hytaleServerVersion.equalsIgnoreCase("*") ? "VERSION" : hytaleServerVersion; + writeText(rootDir, "pom.xml", pomXml(groupId, artifactId, projectVersion, pomVersion)); + } + + private void generateSourcesAndResources(VirtualFile srcMainJava, VirtualFile srcMainResources) + throws ConfigurationException { + + requireNonBlank(javaPackage, "Java package"); + requireNonBlank(mainClassName, "Main class name"); + + requireNonBlank(manifestGroup, "manifest Group"); + requireNonBlank(manifestName, "manifest Name"); + requireNonBlank(manifestVersion, "manifest Version"); + requireNonBlank(manifestServerVersion, "manifest ServerVersion"); + + final String pkgPath = javaPackage.replace('.', '/'); + final VirtualFile pkgDir = mkdirs(srcMainJava, pkgPath); + + writeText(pkgDir, mainClassName + ".java", hytaleMainClass(javaPackage, mainClassName)); + writeText(srcMainResources, "manifest.json", hytaleManifestJson()); + } + + private String hytaleManifestJson() throws ConfigurationException { + final String mainFqn = javaPackage + "." + mainClassName; + + final Map deps = toMapValidated("Dependencies", dependencies); + final Map optDeps = toMapValidated("OptionalDependencies", optionalDependencies); + final Map lb = toMapValidated("LoadBefore", loadBefore); + + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + sb.append(" \"Group\": ").append(jsonString(manifestGroup)).append(",\n"); + sb.append(" \"Name\": ").append(jsonString(manifestName)).append(",\n"); + sb.append(" \"Version\": ").append(jsonString(manifestVersion)).append(",\n"); + sb.append(" \"Description\": ").append(jsonString(nullToEmpty(manifestDescription))).append(",\n"); + sb.append(" \"Main\": ").append(jsonString(mainFqn)).append(",\n"); + sb.append(" \"Website\": ").append(jsonString(nullToEmpty(manifestWebsite))).append(",\n"); + sb.append(" \"Authors\": ").append(authorsJsonArray()).append(",\n"); + sb.append(" \"ServerVersion\": ").append(jsonString(manifestServerVersion)).append(",\n"); + sb.append(" \"Dependencies\": ").append(jsonObject(deps)).append(",\n"); + sb.append(" \"OptionalDependencies\": ").append(jsonObject(optDeps)).append(",\n"); + sb.append(" \"LoadBefore\": ").append(jsonObject(lb)).append(",\n"); + sb.append(" \"DisabledByDefault\": ").append(manifestDisabledByDefault).append(",\n"); + sb.append(" \"IncludesAssetPack\": ").append(manifestIncludesAssetPack).append("\n"); + sb.append("}\n"); + return sb.toString(); + } + + private String authorsJsonArray() throws ConfigurationException { + StringBuilder sb = new StringBuilder(); + sb.append("["); + + boolean first = true; + for (int i = 0; i < authors.size(); i++) { + AuthorRow r = authors.get(i); + String name = safeTrim(r == null ? null : r.getName()); + String website = safeTrim(r == null ? null : r.getWebsite()); + + boolean empty = name.isEmpty() && website.isEmpty(); + if (empty) continue; + + if (name.isEmpty()) { + throw new ConfigurationException("Authors row " + (i + 1) + " is missing Name."); + } + + if (!first) sb.append(", "); + first = false; + + sb.append("{ "); + sb.append("\"Name\": ").append(jsonString(name)); + if (!website.isEmpty()) { + sb.append(", \"Website\": ").append(jsonString(website)); + } + sb.append(" }"); + } + + sb.append("]"); + return sb.toString(); + } + + private static Map toMapValidated(String field, List rows) + throws ConfigurationException { + + Map map = new LinkedHashMap<>(); + if (rows == null) return map; + + for (int i = 0; i < rows.size(); i++) { + KeyValueRow r = rows.get(i); + String k = safeTrim(r == null ? null : r.getKey()); + String v = safeTrim(r == null ? null : r.getValue()); + + boolean empty = k.isEmpty() && v.isEmpty(); + if (empty) continue; + + if (k.isEmpty()) throw new ConfigurationException(field + " row " + (i + 1) + " is missing Key."); + if (v.isEmpty()) throw new ConfigurationException(field + " row " + (i + 1) + " is missing Value."); + + if (map.containsKey(k)) { + throw new ConfigurationException(field + " contains duplicate key: " + k); + } + map.put(k, v); + } + + return map; + } + + private static String jsonObject(Map map) { + if (map == null || map.isEmpty()) return "{}"; + + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + + int i = 0; + for (Map.Entry e : map.entrySet()) { + sb.append(" ").append(jsonString(e.getKey())).append(": ").append(jsonString(e.getValue())); + i++; + if (i < map.size()) sb.append(","); + sb.append("\n"); + } + + sb.append(" }"); + return sb.toString(); + } + + private static String jsonString(String s) { + String v = s == null ? "" : s; + return "\"" + v + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t") + "\""; + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + + private static String safeTrim(String s) { + return s == null ? "" : s.trim(); + } + + private static String pomXml(String groupId, String artifactId, String projectVersion, String hytaleDependencyVersion) + throws ConfigurationException { + + String hv = requireMavenVersion(hytaleDependencyVersion, "Hytale dependency version"); + + return "\n" + + "\n" + + " 4.0.0\n" + + "\n" + + " " + xmlEscape(groupId) + "\n" + + " " + xmlEscape(artifactId) + "\n" + + " " + xmlEscape(projectVersion) + "\n" + + " jar\n" + + "\n" + + " \n" + + " 25\n" + + " 25\n" + + " UTF-8\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " com.hypixel.hytale\n" + + " Server\n" + + " " + xmlEscape(hv) + "\n" + + " \n" + + " \n" + + "\n"; + } + + private static String requireMavenVersion(String value, String fieldName) throws ConfigurationException { + String v = value == null ? "" : value.trim(); + if (v.isEmpty()) { + throw new ConfigurationException(fieldName + " must not be empty."); + } + if (v.contains("*") || v.contains("?")) { + throw new ConfigurationException(fieldName + " must not contain '*' or '?'."); + } + return v; + } + + + private static final String HYTALE_WIZARD_KEY = "dev.unlegitdqrk.hytaleWizardProject"; + + @Override + public @NotNull List commit(@NotNull Project project, + @Nullable com.intellij.openapi.module.ModifiableModuleModel model, + @Nullable com.intellij.openapi.roots.ui.configuration.ModulesProvider modulesProvider) { + List modules = super.commit(project, model, modulesProvider); + if (modules == null || modules.isEmpty()) { + return modules == null ? List.of() : modules; + } + + StartupManager.getInstance(project).runAfterOpened(() -> { + if (project.isDisposed()) return; + + ApplicationManager.getApplication().invokeLater(() -> { + if (project.isDisposed()) return; + + PropertiesComponent.getInstance(project).setValue(HYTALE_WIZARD_KEY, true); + + // Ensure Project View repaints after Maven re-import/reload + ProjectView.getInstance(project).refresh(); + }, ModalityState.nonModal(), project.getDisposed()); + }); + + scheduleMavenImportAfterProjectOpened(project); + return modules; + } + + /** + * Imports the generated Maven project asynchronously without forcing a synchronous full update. + * + *

Do NOT call forceUpdateAllProjectsOrFindAllAvailablePomFiles() here. + * It can trigger synchronous update paths (updateAllMavenProjectsSync) under locks.

+ * + * @param project the IntelliJ project + */ + private void importMavenProjectInBackground(@NotNull Project project) { + try { + if (project.isDisposed()) return; + + String contentPath = getContentEntryPath(); + if (contentPath == null || contentPath.isBlank()) return; + + Path pomPath = Path.of(contentPath).resolve("pom.xml"); + VirtualFile pomVf = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(pomPath); + if (pomVf == null || !pomVf.isValid() || project.isDisposed()) return; + + MavenProjectsManager maven = MavenProjectsManager.getInstance(project); + + List managed = maven.getProjectsFiles(); + if (!managed.contains(pomVf)) { + // IMPORTANT: This must NOT run on the EDT (it can touch Maven Wrapper + VFS). + maven.addManagedFiles(List.of(pomVf)); + } + + // Triggers import/resolve asynchronously. + maven.scheduleImportAndResolve(); + } catch (ProcessCanceledException pce) { + throw pce; + } catch (Throwable ignored) { + // Intentionally swallow to avoid breaking project creation flow. + // If you want, log via Logger.getInstance(...).warn(...) + } + } + + /** + * Schedules Maven import only after the project is fully opened. + * + *

Triggering Maven update/resolve too early (during project creation) can lead to + * modal sync updates and lock contention inside MavenProjectsManager, which in turn + * may freeze the UI or stall coroutines.

+ * + * @param project the IntelliJ project + */ + private void scheduleMavenImportAfterProjectOpened(@NotNull Project project) { + StartupManager.getInstance(project).runAfterOpened(() -> { + if (project.isDisposed()) return; + ApplicationManager.getApplication().executeOnPooledThread(() -> importMavenProjectInBackground(project)); + }); + } + + private static String xmlEscape(String s) { + String v = s == null ? "" : s; + return v.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private static String hytaleMainClass(String pkg, String cls) { + return "package " + pkg + ";\n\n" + + "import com.hypixel.hytale.server.core.plugin.JavaPlugin;\n" + + "import com.hypixel.hytale.server.core.plugin.JavaPluginInit;\n" + + "\n" + + "import javax.annotation.Nonnull;\n" + + "import java.util.logging.Level;\n\n" + + "/**\n" + + " * Hytale server plugin.\n" + + " */\n" + + "public class " + cls + " extends JavaPlugin {\n\n" + + " private static " + cls + " instance;\n\n" + + " /**\n" + + " * Creates the plugin instance.\n" + + " *\n" + + " * @param init plugin initialization context\n" + + " */\n" + + " public " + cls + "(@Nonnull JavaPluginInit init) {\n" + + " super(init);\n" + + " }\n\n" + + " /**\n" + + " * @return plugin singleton instance\n" + + " */\n" + + " public static " + cls + " get() {\n" + + " return instance;\n" + + " }\n\n" + + " @Override\n" + + " protected void setup() {\n" + + " instance = this;\n" + + " getLogger().at(Level.INFO).log(\"Plugin setup complete!\");\n" + + " super.setup();\n" + + " }\n\n" + + " @Override\n" + + " protected void start() {\n" + + " getLogger().at(Level.INFO).log(\"Plugin started!\");\n" + + " super.start();\n" + + " }\n\n" + + " @Override\n" + + " protected void shutdown() {\n" + + " getLogger().at(Level.INFO).log(\"Plugin shutting down!\");\n" + + " super.shutdown();\n" + + " }\n" + + "}\n"; + } + + private static VirtualFile createAndGetRootDir(String contentPath) throws ConfigurationException { + try { + return WriteAction.computeAndWait(() -> { + FileUtil.createDirectory(new java.io.File(contentPath)); + VirtualFile vf = VfsUtil.findFileByIoFile(new java.io.File(contentPath), true); + if (vf == null) { + throw new IOException("Failed to resolve content root: " + contentPath); + } + return vf; + }); + } catch (ProcessCanceledException pce) { + // Must not be swallowed; IntelliJ uses this for cooperative cancellation. + throw pce; + } catch (Throwable t) { + throw new ConfigurationException("Failed to create project root: " + t.getMessage(), t.getMessage()); + } + } + + private static VirtualFile mkdirs(VirtualFile root, String relPath) throws ConfigurationException { + try { + return WriteAction.computeAndWait(() -> VfsUtil.createDirectories(root.getPath() + "/" + relPath)); + } catch (ProcessCanceledException pce) { + throw pce; + } catch (IOException e) { + throw new ConfigurationException("Failed to create directories: " + relPath, e.getMessage()); + } + } + + private static void writeText(VirtualFile dir, String name, String content) throws ConfigurationException { + try { + WriteAction.run(() -> { + VirtualFile f = dir.findChild(name); + if (f == null) { + f = dir.createChildData(CustomProjectModuleBuilder.class, name); + } + f.setBinaryContent(content.getBytes(StandardCharsets.UTF_8)); + }); + } catch (ProcessCanceledException pce) { + throw pce; + } catch (IOException ioe) { + throw new ConfigurationException( + "Failed to write file: " + dir.getPath() + "/" + name + " (" + ioe.getMessage() + ")", + ioe.getMessage() + ); + } catch (RuntimeException re) { + Throwable cause = re.getCause() == null ? re : re.getCause(); + throw new ConfigurationException( + "Failed to write file: " + dir.getPath() + "/" + name + " (" + cause.getMessage() + ")", + cause.getMessage() + ); + } + } + + private static void requireNonBlank(String v, String field) throws ConfigurationException { + if (isBlank(v)) { + throw new ConfigurationException(field + " must not be empty."); + } + } + + private static boolean isBlank(String v) { + return v == null || v.trim().isEmpty(); + } + + // --- setters used by wizard step --- + /** + * Sets the JDK selected in the wizard UI. + * + * @param selectedJdk selected JDK (may be null to inherit) + */ + public void setSelectedJdk(Sdk selectedJdk) { + this.selectedJdk = selectedJdk; + } + + public void setGroupId(String groupId) { this.groupId = groupId; } + public void setArtifactId(String artifactId) { this.artifactId = artifactId; } + public void setProjectVersion(String projectVersion) { this.projectVersion = projectVersion; } + + public void setJavaPackage(String javaPackage) { this.javaPackage = javaPackage; } + public void setMainClassName(String mainClassName) { this.mainClassName = mainClassName; } + + public void setHytaleServerVersion(String hytaleServerVersion) { this.hytaleServerVersion = hytaleServerVersion; } + + public void setManifestGroup(String manifestGroup) { this.manifestGroup = manifestGroup; } + public void setManifestName(String manifestName) { this.manifestName = manifestName; } + public void setManifestVersion(String manifestVersion) { this.manifestVersion = manifestVersion; } + public void setManifestDescription(String manifestDescription) { this.manifestDescription = manifestDescription; } + public void setManifestWebsite(String manifestWebsite) { this.manifestWebsite = manifestWebsite; } + public void setManifestServerVersion(String manifestServerVersion) { this.manifestServerVersion = manifestServerVersion; } + + public void setAuthors(List authors) { this.authors = authors == null ? List.of() : authors; } + public void setDependencies(List dependencies) { this.dependencies = dependencies == null ? List.of() : dependencies; } + public void setOptionalDependencies(List optionalDependencies) { this.optionalDependencies = optionalDependencies == null ? List.of() : optionalDependencies; } + public void setLoadBefore(List loadBefore) { this.loadBefore = loadBefore == null ? List.of() : loadBefore; } + + public void setManifestDisabledByDefault(boolean manifestDisabledByDefault) { this.manifestDisabledByDefault = manifestDisabledByDefault; } + public void setManifestIncludesAssetPack(boolean manifestIncludesAssetPack) { this.manifestIncludesAssetPack = manifestIncludesAssetPack; } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/CustomProjectWizardStep.java b/src/main/java/dev/unlegitdqrk/wizard/CustomProjectWizardStep.java new file mode 100644 index 0000000..2f15f92 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/CustomProjectWizardStep.java @@ -0,0 +1,494 @@ +package dev.unlegitdqrk.wizard; + +import com.intellij.ide.util.projectWizard.ModuleWizardStep; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkTypeId; +import com.intellij.openapi.roots.ui.configuration.JdkComboBox; +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; +import com.intellij.openapi.util.Condition; +import dev.unlegitdqrk.wizard.table.AuthorTableModel; +import dev.unlegitdqrk.wizard.table.KeyValueTableModel; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Wizard UI for creating a Hytale Java plugin project (Maven only). + * + *

Auto bindings: + *

    + *
  • groupId -> Java package, Manifest Group
  • + *
  • artifactId -> Main class, Manifest Name
  • + *
  • version -> Manifest Version
  • + *
+ * + *

Bindings are disabled for a target field once the user edits that target field manually. + */ +public final class CustomProjectWizardStep extends ModuleWizardStep { + + private static final Pattern JAVA_PACKAGE = + Pattern.compile("[A-Za-z_$][A-Za-z\\d_$]*(\\.[A-Za-z_$][A-Za-z\\d_$]*)*"); + private static final Pattern JAVA_IDENTIFIER = + Pattern.compile("[A-Za-z_$][A-Za-z\\d_$]*"); + + private final CustomProjectModuleBuilder builder; + + private JPanel root; + + // Maven + private JTextField groupId; + private JTextField artifactId; + private JTextField version; + private JTextField hytaleServerVersion; + + // Java + private JTextField javaPackage; + private JTextField mainClass; + + // Manifest + private JTextField manifestGroup; + private JTextField manifestName; + private JTextField manifestVersion; + private JTextArea manifestDescription; + private JTextField manifestWebsite; + private JTextField manifestServerVersion; + + private AuthorTableModel authorsModel; + private KeyValueTableModel depsModel; + private KeyValueTableModel optDepsModel; + private KeyValueTableModel loadBeforeModel; + + private JCheckBox manifestDisabledByDefault; + private JCheckBox manifestIncludesAssetPack; + + // JDK + private ProjectSdksModel sdksModel; + private JdkComboBox jdkCombo; + + /** + * Creates the wizard step. + * + * @param builder module builder to store user selections into + */ + public CustomProjectWizardStep(@NotNull CustomProjectModuleBuilder builder) { + this.builder = builder; + initUi(); + installAutoBindings(); + } + + @Override + public JComponent getComponent() { + return new JScrollPane(root); + } + + @Override + public void updateDataModel() { + // Maven + builder.setGroupId(trim(groupId.getText())); + builder.setArtifactId(trim(artifactId.getText())); + builder.setProjectVersion(trim(version.getText())); + builder.setHytaleServerVersion(trim(hytaleServerVersion.getText())); + + // Java + builder.setJavaPackage(trim(javaPackage.getText())); + builder.setMainClassName(trim(mainClass.getText())); + + // Manifest + builder.setManifestGroup(trim(manifestGroup.getText())); + builder.setManifestName(trim(manifestName.getText())); + builder.setManifestVersion(trim(manifestVersion.getText())); + builder.setManifestDescription(trim(manifestDescription.getText())); + builder.setManifestWebsite(trim(manifestWebsite.getText())); + builder.setManifestServerVersion(trim(manifestServerVersion.getText())); + + builder.setAuthors(authorsModel.getRows()); + builder.setDependencies(depsModel.getRows()); + builder.setOptionalDependencies(optDepsModel.getRows()); + builder.setLoadBefore(loadBeforeModel.getRows()); + + builder.setManifestDisabledByDefault(manifestDisabledByDefault.isSelected()); + builder.setManifestIncludesAssetPack(manifestIncludesAssetPack.isSelected()); + + try { + sdksModel.apply(); + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + + Sdk selected = jdkCombo.getSelectedJdk(); + builder.setSelectedJdk(selected); + } + + @Override + public boolean validate() throws ConfigurationException { + if (trim(groupId.getText()).isEmpty()) throw new ConfigurationException("groupId must not be empty."); + if (trim(artifactId.getText()).isEmpty()) throw new ConfigurationException("artifactId must not be empty."); + if (trim(version.getText()).isEmpty()) throw new ConfigurationException("version must not be empty."); + if (trim(hytaleServerVersion.getText()).isEmpty()) throw new ConfigurationException("Hytale Server Version must not be empty."); + + String pkg = trim(javaPackage.getText()); + if (pkg.isEmpty() || !JAVA_PACKAGE.matcher(pkg).matches()) { + throw new ConfigurationException("Invalid Java package."); + } + + String cls = trim(mainClass.getText()); + if (cls.isEmpty() || !JAVA_IDENTIFIER.matcher(cls).matches()) { + throw new ConfigurationException("Invalid main class name."); + } + + if (trim(manifestGroup.getText()).isEmpty()) throw new ConfigurationException("Manifest Group must not be empty."); + if (trim(manifestName.getText()).isEmpty()) throw new ConfigurationException("Manifest Name must not be empty."); + if (trim(manifestVersion.getText()).isEmpty()) throw new ConfigurationException("Manifest Version must not be empty."); + if (trim(manifestServerVersion.getText()).isEmpty()) throw new ConfigurationException("Manifest ServerVersion must not be empty."); + + // JDK required? If you want to enforce: + // if (jdkCombo.getSelectedJdk() == null) throw new ConfigurationException("Please select a JDK."); + + return true; + } + + private void initUi() { + root = new JPanel(); + root.setLayout(new BoxLayout(root, BoxLayout.Y_AXIS)); + + // --- Maven fields --- + groupId = new JTextField(""); + artifactId = new JTextField(); + version = new JTextField("1.0.0"); + hytaleServerVersion = new JTextField("*"); + + // --- Java fields --- + javaPackage = new JTextField(); + mainClass = new JTextField(); + + // --- Manifest fields --- + manifestGroup = new JTextField(); + manifestName = new JTextField(); + manifestVersion = new JTextField(); + manifestDescription = new JTextArea(3, 40); + manifestWebsite = new JTextField(); + manifestServerVersion = new JTextField("*"); + + authorsModel = new AuthorTableModel(); + depsModel = new KeyValueTableModel(); + optDepsModel = new KeyValueTableModel(); + loadBeforeModel = new KeyValueTableModel(); + + manifestDisabledByDefault = new JCheckBox("DisabledByDefault", false); + manifestIncludesAssetPack = new JCheckBox("IncludesAssetPack", false); + + // --- JDK combo (only Java SDKs) --- + sdksModel = new ProjectSdksModel(); + sdksModel.reset(null); // loads SDKs from ProjectJdkTable + + Condition onlyJavaSdks = typeId -> typeId instanceof JavaSdk; + + jdkCombo = new JdkComboBox( + (Project) null, + sdksModel, + onlyJavaSdks, // sdkTypeFilter + null, // sdkFilter + onlyJavaSdks, // creationFilter (only allow creating Java SDKs) + null // onNewSdkAdded + ); + + // --- Collapsible sections like Minecraft wizard --- + CollapsibleSection maven = new CollapsibleSection("Maven", true); + maven.getContent().add(formRow("groupId:", groupId)); + maven.getContent().add(formRow("artifactId:", artifactId)); + maven.getContent().add(formRow("version:", version)); + maven.getContent().add(formRow("Hytale Server Version:", hytaleServerVersion)); + + CollapsibleSection java = new CollapsibleSection("Java", true); + java.getContent().add(formRow("package:", javaPackage)); + java.getContent().add(formRow("Main class:", mainClass)); + + CollapsibleSection buildSystemProps = new CollapsibleSection("Build System Properties", false); + buildSystemProps.getContent().add(formRow("JDK:", jdkCombo)); + + CollapsibleSection manifest = new CollapsibleSection("Manifest", true); + manifest.getContent().add(formRow("Group:", manifestGroup)); + manifest.getContent().add(formRow("Name:", manifestName)); + manifest.getContent().add(formRow("Version:", manifestVersion)); + manifest.getContent().add(formRow("Website:", manifestWebsite)); + manifest.getContent().add(formRow("ServerVersion:", manifestServerVersion)); + manifest.getContent().add(textAreaRow("Description:", manifestDescription)); + + // Compact tables (smaller rows like you requested) + JPanel flags = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + flags.add(manifestDisabledByDefault); + flags.add(manifestIncludesAssetPack); + manifest.getContent().add(flags); + + manifest.getContent().add(compactTableBlock("Authors", authorsModel)); + manifest.getContent().add(compactTableBlock("Dependencies", depsModel)); + manifest.getContent().add(compactTableBlock("OptionalDependencies", optDepsModel)); + manifest.getContent().add(compactTableBlock("LoadBefore", loadBeforeModel)); + + // Layout + root.add(maven); + root.add(Box.createVerticalStrut(6)); + root.add(java); + root.add(Box.createVerticalStrut(6)); + root.add(buildSystemProps); + root.add(Box.createVerticalStrut(6)); + root.add(manifest); + } + + private void installAutoBindings() { + new OneWayAutoBinding(groupId, javaPackage, s -> s); + new OneWayAutoBinding(groupId, manifestGroup, s -> s); + + new OneWayAutoBinding(artifactId, mainClass, CustomProjectWizardStep::artifactIdToJavaClassName); + new OneWayAutoBinding(artifactId, manifestName, s -> s); + + new OneWayAutoBinding(version, manifestVersion, s -> s); + } + + private static JPanel formRow(String label, JComponent field) { + JPanel row = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(4, 6, 4, 6); + c.fill = GridBagConstraints.HORIZONTAL; + + c.gridx = 0; + c.gridy = 0; + c.weightx = 0; + row.add(new JLabel(label), c); + + c.gridx = 1; + c.weightx = 1; + row.add(field, c); + + return row; + } + + private static JPanel textAreaRow(String label, JTextArea area) { + JPanel row = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(4, 6, 4, 6); + c.fill = GridBagConstraints.HORIZONTAL; + + c.gridx = 0; + c.gridy = 0; + c.weightx = 0; + row.add(new JLabel(label), c); + + c.gridx = 1; + c.weightx = 1; + JScrollPane sp = new JScrollPane(area); + sp.setPreferredSize(new Dimension(400, 72)); + row.add(sp, c); + + return row; + } + + private static JPanel compactTableBlock(String title, Object model) { + JPanel block = new JPanel(new BorderLayout()); + block.setBorder(BorderFactory.createTitledBorder(title)); + + JTable table = new JTable((javax.swing.table.TableModel) model); + table.setFillsViewportHeight(true); + + // Smaller rows + table.setRowHeight(18); + + // Compact viewport height (~5 rows) + JScrollPane sp = new JScrollPane(table); + sp.setPreferredSize(new Dimension(520, table.getRowHeight() * 6)); + + block.add(sp, BorderLayout.CENTER); + + JButton add = new JButton("Add"); + JButton remove = new JButton("Remove"); + + add.addActionListener(e -> { + if (model instanceof AuthorTableModel m) m.addRow(); + if (model instanceof KeyValueTableModel m) m.addRow(); + }); + + remove.addActionListener(e -> { + int idx = table.getSelectedRow(); + if (idx < 0) return; + if (model instanceof AuthorTableModel m) m.removeRow(idx); + if (model instanceof KeyValueTableModel m) m.removeRow(idx); + }); + + JPanel buttons = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + buttons.add(add); + buttons.add(remove); + + block.add(buttons, BorderLayout.SOUTH); + return block; + } + + private static String trim(String s) { + return s == null ? "" : s.trim(); + } + + /** + * Converts an artifactId into a best-effort valid Java class name. + * + * @param artifactId artifactId + * @return Java identifier + */ + private static String artifactIdToJavaClassName(String artifactId) { + String in = trim(artifactId); + if (in.isEmpty()) return ""; + + StringBuilder out = new StringBuilder(); + boolean capNext = true; + + for (int i = 0; i < in.length(); i++) { + char ch = in.charAt(i); + if (Character.isLetterOrDigit(ch)) { + if (out.isEmpty() && Character.isDigit(ch)) { + out.append('_'); + } + if (capNext) { + out.append(Character.toUpperCase(ch)); + capNext = false; + } else { + out.append(ch); + } + } else { + capNext = true; + } + } + + String result = out.toString(); + if (result.isEmpty()) return ""; + + if (!Character.isJavaIdentifierStart(result.charAt(0))) { + result = "_" + result; + } + + StringBuilder safe = new StringBuilder(); + for (int i = 0; i < result.length(); i++) { + char ch = result.charAt(i); + safe.append(Character.isJavaIdentifierPart(ch) ? ch : '_'); + } + return safe.toString(); + } + + /** + * One-way binding: source -> target. + * Auto-updates the target until the user edits the target field manually. + */ + private static final class OneWayAutoBinding { + + private final JTextField source; + private final JTextField target; + private final Transformer transformer; + + private boolean autoEnabled = true; + private boolean updatingProgrammatically = false; + private String lastAutoValue = null; + + OneWayAutoBinding(JTextField source, JTextField target, Transformer transformer) { + this.source = Objects.requireNonNull(source, "source"); + this.target = Objects.requireNonNull(target, "target"); + this.transformer = Objects.requireNonNull(transformer, "transformer"); + + this.target.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { onTargetChanged(); } + @Override public void removeUpdate(DocumentEvent e) { onTargetChanged(); } + @Override public void changedUpdate(DocumentEvent e) { onTargetChanged(); } + + private void onTargetChanged() { + if (updatingProgrammatically) return; + if (lastAutoValue != null && !trim(target.getText()).equals(lastAutoValue)) { + autoEnabled = false; + } + } + }); + + this.source.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { apply(); } + @Override public void removeUpdate(DocumentEvent e) { apply(); } + @Override public void changedUpdate(DocumentEvent e) { apply(); } + }); + + apply(); + } + + private void apply() { + if (!autoEnabled) return; + + String src = trim(source.getText()); + String next = trim(transformer.transform(src)); + + updatingProgrammatically = true; + try { + target.setText(next); + lastAutoValue = next; + } finally { + updatingProgrammatically = false; + } + } + + @FunctionalInterface + private interface Transformer { + String transform(String value); + } + } + + /** + * Simple collapsible panel (like "Optional Settings" / "Build System Properties"). + */ + private static final class CollapsibleSection extends JPanel { + + private static final Icon EXPANDED_ICON = + com.intellij.icons.AllIcons.General.ArrowDown; + private static final Icon COLLAPSED_ICON = + com.intellij.icons.AllIcons.General.ArrowRight; + + private final JPanel content; + private final JToggleButton toggle; + + CollapsibleSection(String title, boolean expanded) { + super(new BorderLayout()); + + toggle = new JToggleButton(title); + toggle.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); + toggle.setFocusPainted(false); + toggle.setHorizontalAlignment(SwingConstants.LEFT); + toggle.setHorizontalTextPosition(SwingConstants.RIGHT); + toggle.setIconTextGap(8); + + content = new JPanel(); + content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); + + add(toggle, BorderLayout.NORTH); + add(content, BorderLayout.CENTER); + + setExpanded(expanded); + + toggle.addActionListener(e -> setExpanded(toggle.isSelected())); + } + + JPanel getContent() { + return content; + } + + private void setExpanded(boolean expanded) { + toggle.setSelected(expanded); + content.setVisible(expanded); + + // Visual state indicator + toggle.setIcon(expanded ? EXPANDED_ICON : COLLAPSED_ICON); + + // Ensure layout refresh + revalidate(); + repaint(); + } + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/HytaleModuleType.java b/src/main/java/dev/unlegitdqrk/wizard/HytaleModuleType.java new file mode 100644 index 0000000..29584dd --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/HytaleModuleType.java @@ -0,0 +1,43 @@ +package dev.unlegitdqrk.wizard; + +import com.intellij.ide.util.projectWizard.ModuleBuilder; +import com.intellij.openapi.module.ModuleType; +import com.intellij.openapi.util.IconLoader; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * Module type entry for the New Project dialog. + */ +public final class HytaleModuleType extends ModuleType { + + public static final String ID = "HYLATE_PLUGIN_MODULE"; + + /** + * Creates the module type. + */ + public HytaleModuleType() { + super(ID); + } + + @Override + public @NotNull ModuleBuilder createModuleBuilder() { + return new CustomProjectModuleBuilder(); + } + + @Override + public @NotNull String getName() { + return "Hytale Plugin (Maven)"; + } + + @Override + public @NotNull String getDescription() { + return "Creates a Hytale server plugin project (Java + Maven) with manifest.json."; + } + + @Override + public Icon getNodeIcon(boolean isOpened) { + return IconLoader.getIcon("/icon/icon.png", HytaleModuleType.class); + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectDetectionService.java b/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectDetectionService.java new file mode 100644 index 0000000..86aa7e7 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectDetectionService.java @@ -0,0 +1,110 @@ +package dev.unlegitdqrk.wizard; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.*; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; + +/** + * Detects whether a project is a Hytale plugin project by checking the Maven pom.xml + * for the dependency com.hypixel.hytale:Server. The result is cached and updated + * on pom.xml changes. + */ +@Service(Service.Level.PROJECT) +public final class HytaleProjectDetectionService { + + private final Project project; + private final AtomicBoolean hytale = new AtomicBoolean(false); + + public HytaleProjectDetectionService(@NotNull Project project) { + this.project = project; + + // Initial detection after project is ready (off EDT). + scheduleRescan(); + + // Track pom.xml modifications to support "after-the-fact" projects. + project.getMessageBus().connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() { + @Override + public void after(@NotNull java.util.List events) { + if (project.isDisposed()) return; + + boolean relevant = false; + for (VFileEvent e : events) { + VirtualFile f = e.getFile(); + if (f == null) continue; + if ("pom.xml".equalsIgnoreCase(f.getName())) { + relevant = true; + break; + } + } + if (relevant) { + scheduleRescan(); + } + } + }); + } + + /** + * @return true if the project currently looks like a Hytale Maven project + */ + public boolean isHytaleProject() { + return hytale.get(); + } + + private void scheduleRescan() { + ApplicationManager.getApplication().executeOnPooledThread(this::rescanNow); + } + + private void rescanNow() { + if (project.isDisposed()) return; + + VirtualFile base = project.getBaseDir(); + if (base == null || !base.isValid()) { + hytale.set(false); + return; + } + + VirtualFile pom = base.findChild("pom.xml"); + if (pom == null || !pom.isValid()) { + hytale.set(false); + return; + } + + hytale.set(hasHytaleDependency(pom)); + } + + private static boolean hasHytaleDependency(@NotNull VirtualFile pomVf) { + try (InputStream in = pomVf.getInputStream()) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + Document doc = dbf.newDocumentBuilder().parse(in); + + var xp = XPathFactory.newInstance().newXPath(); + + // Match com.hypixel.hytaleServer + String expr = + "boolean(//*[local-name()='dependency']" + + "[./*[local-name()='groupId' and normalize-space(text())='com.hypixel.hytale']" + + " and ./*[local-name()='artifactId' and normalize-space(text())='Server']])"; + + Boolean result = (Boolean) xp.evaluate(expr, doc, XPathConstants.BOOLEAN); + return Boolean.TRUE.equals(result); + } catch (Exception ignored) { + return false; + } + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectViewDecorator.java b/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectViewDecorator.java new file mode 100644 index 0000000..a8162c9 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/HytaleProjectViewDecorator.java @@ -0,0 +1,78 @@ +package dev.unlegitdqrk.wizard; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.ide.projectView.ProjectViewNode; +import com.intellij.ide.projectView.ProjectViewNodeDecorator; +import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * Sets the Hytale icon for the project content root directory node in the Project view. + * + *

This mirrors the approach used by Minecraft Development (mcdev): only decorate the directory node + * representing a module content root, never files/folders below it.

+ */ +public final class HytaleProjectViewDecorator implements ProjectViewNodeDecorator { + + private static final Icon ICON = IconLoader.getIcon("/icon/icon.png", HytaleProjectViewDecorator.class); + + @Override + public void decorate(@NotNull ProjectViewNode node, @NotNull PresentationData data) { + if (!(node instanceof PsiDirectoryNode)) { + return; + } + + PsiDirectoryNode dirNode = (PsiDirectoryNode) node; + if (!dirNode.isValid()) { + return; + } + + Project project = dirNode.getProject(); + if (project == null || project.isDisposed()) { + return; + } + + HytaleProjectDetectionService svc = project.getService(HytaleProjectDetectionService.class); + if (svc == null || !svc.isHytaleProject()) { + return; + } + + PsiDirectory dir = dirNode.getValue(); + if (dir == null) { + return; + } + + Module module = ModuleUtilCore.findModuleForPsiElement(dir); + if (module == null || module.isDisposed() || !module.isLoaded() || module.getProject().isDisposed()) { + return; + } + + VirtualFile vf = dirNode.getVirtualFile(); + if (vf == null || !vf.isValid()) { + return; + } + + VirtualFile[] roots = ModuleRootManager.getInstance(module).getContentRoots(); + boolean isContentRoot = false; + for (VirtualFile r : roots) { + if (vf.equals(r)) { + isContentRoot = true; + break; + } + } + if (!isContentRoot) { + return; + } + + data.setIcon(ICON); + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/table/AuthorRow.java b/src/main/java/dev/unlegitdqrk/wizard/table/AuthorRow.java new file mode 100644 index 0000000..d914410 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/table/AuthorRow.java @@ -0,0 +1,49 @@ +package dev.unlegitdqrk.wizard.table; + +/** + * Represents a single author entry in manifest.json. + */ +public final class AuthorRow { + + private String name; + private String website; + + /** + * Creates an author row. + * + * @param name author name + * @param website author website (optional) + */ + public AuthorRow(String name, String website) { + this.name = name; + this.website = website; + } + + /** + * @return author name + */ + public String getName() { + return name; + } + + /** + * @param name author name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return author website + */ + public String getWebsite() { + return website; + } + + /** + * @param website author website + */ + public void setWebsite(String website) { + this.website = website; + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/table/AuthorTableModel.java b/src/main/java/dev/unlegitdqrk/wizard/table/AuthorTableModel.java new file mode 100644 index 0000000..7b0eff7 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/table/AuthorTableModel.java @@ -0,0 +1,81 @@ +package dev.unlegitdqrk.wizard.table; + +import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Table model for the Authors array in manifest.json. + */ +public final class AuthorTableModel extends AbstractTableModel { + + private static final String[] COLUMNS = {"Name", "Website"}; + + private final List rows = new ArrayList<>(); + + @Override + public int getRowCount() { + return rows.size(); + } + + @Override + public int getColumnCount() { + return COLUMNS.length; + } + + @Override + public String getColumnName(int column) { + return COLUMNS[column]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + AuthorRow row = rows.get(rowIndex); + return columnIndex == 0 ? row.getName() : row.getWebsite(); + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + AuthorRow row = rows.get(rowIndex); + String v = value == null ? "" : value.toString(); + + if (columnIndex == 0) { + row.setName(v); + } else { + row.setWebsite(v); + } + fireTableCellUpdated(rowIndex, columnIndex); + } + + /** + * Adds a new empty row. + */ + public void addRow() { + rows.add(new AuthorRow("", "")); + fireTableRowsInserted(rows.size() - 1, rows.size() - 1); + } + + /** + * Removes a row. + * + * @param index row index + */ + public void removeRow(int index) { + if (index < 0 || index >= rows.size()) return; + rows.remove(index); + fireTableRowsDeleted(index, index); + } + + /** + * @return immutable list of rows + */ + public List getRows() { + return Collections.unmodifiableList(rows); + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueRow.java b/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueRow.java new file mode 100644 index 0000000..724c893 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueRow.java @@ -0,0 +1,49 @@ +package dev.unlegitdqrk.wizard.table; + +/** + * Generic key-value row for dependency maps in manifest.json. + */ +public final class KeyValueRow { + + private String key; + private String value; + + /** + * Creates a key-value row. + * + * @param key key (e.g. "Hytale:SomePlugin") + * @param value value (e.g. ">=1.0.0" or "*") + */ + public KeyValueRow(String key, String value) { + this.key = key; + this.value = value; + } + + /** + * @return key + */ + public String getKey() { + return key; + } + + /** + * @param key key + */ + public void setKey(String key) { + this.key = key; + } + + /** + * @return value + */ + public String getValue() { + return value; + } + + /** + * @param value value + */ + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueTableModel.java b/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueTableModel.java new file mode 100644 index 0000000..54b90a9 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/wizard/table/KeyValueTableModel.java @@ -0,0 +1,81 @@ +package dev.unlegitdqrk.wizard.table; + +import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Table model for dependency maps in manifest.json. + */ +public final class KeyValueTableModel extends AbstractTableModel { + + private static final String[] COLUMNS = {"Key", "Value"}; + + private final List rows = new ArrayList<>(); + + @Override + public int getRowCount() { + return rows.size(); + } + + @Override + public int getColumnCount() { + return COLUMNS.length; + } + + @Override + public String getColumnName(int column) { + return COLUMNS[column]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + KeyValueRow row = rows.get(rowIndex); + return columnIndex == 0 ? row.getKey() : row.getValue(); + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + KeyValueRow row = rows.get(rowIndex); + String v = value == null ? "" : value.toString(); + + if (columnIndex == 0) { + row.setKey(v); + } else { + row.setValue(v); + } + fireTableCellUpdated(rowIndex, columnIndex); + } + + /** + * Adds a new empty row. + */ + public void addRow() { + rows.add(new KeyValueRow("", "")); + fireTableRowsInserted(rows.size() - 1, rows.size() - 1); + } + + /** + * Removes a row. + * + * @param index row index + */ + public void removeRow(int index) { + if (index < 0 || index >= rows.size()) return; + rows.remove(index); + fireTableRowsDeleted(index, index); + } + + /** + * @return immutable list of rows + */ + public List getRows() { + return Collections.unmodifiableList(rows); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..6914f3f --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,22 @@ + + dev.unlegitdqrk.wizard + Hytale Wizard + dev.unlegitdqrk + + com.intellij.modules.java + org.jetbrains.idea.maven + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..b468c4a --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icon/icon.png b/src/main/resources/icon/icon.png new file mode 100644 index 0000000..0e42bdf Binary files /dev/null and b/src/main/resources/icon/icon.png differ diff --git a/src/main/resources/messages/MyMessageBundle.properties b/src/main/resources/messages/MyMessageBundle.properties new file mode 100644 index 0000000..a0157ad --- /dev/null +++ b/src/main/resources/messages/MyMessageBundle.properties @@ -0,0 +1 @@ +toolwindow.stripe.MyToolWindow=Hytale Wizard \ No newline at end of file