forked from libktx/ktx
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tools module and BundleLineCreator for libktx#14
- Loading branch information
1 parent
4d522fe
commit c01bc35
Showing
7 changed files
with
323 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ include( | |
'scene2d', | ||
'style', | ||
'tiled', | ||
'tools', | ||
'vis', | ||
'vis-style', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# KTX: Tools | ||
|
||
Utilities for working with Ktx or generating code for use with LibGDX. | ||
|
||
### Why? | ||
|
||
Like the LibGDX `gdx-tools` library, this library contains tools for development rather than use in your game itself. | ||
|
||
### Guide | ||
|
||
This module contains tools that should be not be included in the core module. It can be imported as a `buildscipt` | ||
`classpath` dependency so its Gradle plugin can be used. The plugin adds Gradle tasks to the project in the `ktx tools` | ||
group. It can be used by adding this to the `build.gradle` file: | ||
|
||
apply plugin: "io.github.libktx.tools" | ||
|
||
The tools can also be used from an application. In this case, import this library as a dependency of a JVM module (not | ||
the `core` game module.) | ||
|
||
#### BundleLineCreator | ||
|
||
`BundleLineCreator` is used to generate `BundleLine` enum classes from properties files for use with `ktx-i18n`. It | ||
searches a directory for `.properties` files, extracts the property names, and adds a `BundleLine` enum class with | ||
corresponding enum values. The default behavior requires only an output package name. It automatically finds the | ||
`assets` directory as found in a project created with LibGDX Setup, and favors limiting its search to a sub-directory | ||
named `i18n` or `nls` if either exists. It combines property names into a single enum class named `Nls`, placed in the | ||
output package in the `core` module, overwriting if necessary. | ||
|
||
To run this default behavior from a JVM app, call `BundleLineCreator.execute("com.mycompany.mygame")`. | ||
|
||
To run it as a Gradle task, you can first set the output package name in the `gradle.build` using | ||
|
||
```groovy | ||
ktxTools { | ||
createBundleLines.targetPackage = "com.mycompany.mygame" | ||
} | ||
``` | ||
|
||
and then running the `createBundleLines` task in the `ktx tools` group. IntelliJ's | ||
[FileWatchers](https://www.jetbrains.com/help/idea/using-file-watchers.html) plugin may be of interest as it can be set | ||
up to run the task when a file changes. | ||
|
||
See usage examples below for how to customize the behavior. Note that `BundleLineCreator` can also be subclassed to | ||
customize its behavior farther. | ||
|
||
## | ||
|
||
### Usage examples | ||
|
||
Using all the customization options when running `BundleLinesCreator`. | ||
|
||
In a JVM app: | ||
|
||
```kotlin | ||
BundleLinesCreator.execute( | ||
targetPackage = "com.mycompany.mygame", | ||
explicitParentDirectory = "android/assets/bundles", | ||
searchSubdirectories = false, | ||
targetSourceDirectory = "mygamecore/src", | ||
enumClassName = "Keys" | ||
) | ||
``` | ||
|
||
In a `build.gradle` file to customize `createBundleLines` task behavior: | ||
|
||
```groovy | ||
ktxTools { | ||
createBundleLines.targetPackage = "com.mycompany.mygame" | ||
createBundleLines.explicitParentDirectory = "android/assets/bundles" | ||
createBundleLines.searchSubdirectories = false | ||
createBundleLines.targetSourceDirectory = "mygamecore/src" | ||
createBundleLines.enumClassName = "Keys" | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
plugins { | ||
id 'java-gradle-plugin' | ||
} | ||
|
||
dependencies { | ||
implementation "commons-io:commons-io:2.4" | ||
} | ||
|
||
gradlePlugin { | ||
plugins { | ||
simplePlugin { | ||
id = 'io.github.libktx.tools' | ||
implementationClass = 'ktx.tools.KtxToolsPlugin' | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
projectName=ktx-tools | ||
projectDesc=Development tools for LibKtx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package ktx.tools | ||
|
||
import org.apache.commons.io.FileUtils | ||
import org.apache.commons.io.filefilter.AbstractFileFilter | ||
import org.apache.commons.io.filefilter.FileFilterUtils | ||
import org.apache.commons.io.filefilter.SuffixFileFilter | ||
import org.apache.commons.io.filefilter.TrueFileFilter | ||
import java.io.File | ||
import java.io.IOException | ||
import java.util.Properties | ||
|
||
/** Searches for properties files and generates Ktx BundleLine class files for them when executed. The companion object | ||
* instance can be used directly or behavior can be customized by subclassing. */ | ||
open class BundleLinesCreator { | ||
|
||
companion object : BundleLinesCreator() | ||
|
||
/** | ||
* Executes creation of enum source files. | ||
* @param targetPackage The package created enums will be placed in. | ||
* @param explicitParentDirectory Directory that is searched for properties files when creating BundleLines. If null | ||
* (the default), a directory is searched for as described by [findParentDirectory]. | ||
* @param searchSubdirectories Whether to search subdirectories of the parent directory. Default true. | ||
* @param targetSourceDirectory The directory enum source files will be placed in. Default `"core/src"`. | ||
* @param enumClassName The name of the generated enum class. If non-null, a single enum class is created for all | ||
* bundles found. If null, each unique base bundle's name will be used for a distinct enum class. Default `"Nls"`. | ||
*/ | ||
fun execute( | ||
targetPackage: String, | ||
explicitParentDirectory: String? = null, | ||
searchSubdirectories: Boolean = true, | ||
targetSourceDirectory: String = "core/src", | ||
enumClassName: String? = "Nls" | ||
) { | ||
try { | ||
val parentDirectory = findParentDirectory(explicitParentDirectory) | ||
if (parentDirectory == null) { | ||
printlnRed("Failed to find asset directory. No files will be created.") | ||
return | ||
} | ||
val baseFiles = findBasePropertiesFiles(parentDirectory, searchSubdirectories) | ||
val enumNamesToEntryNames = collectKeys(baseFiles, enumClassName) | ||
val outDir = File("$targetSourceDirectory/${targetPackage.replace(".", "/")}") | ||
.apply { mkdirs() } | ||
for ((enumName, entryNames) in enumNamesToEntryNames) { | ||
val outFile = File(outDir, "$enumName.kt") | ||
val sourceCode = generateKtFileContent(targetPackage, enumName, entryNames) | ||
outFile.writeText(sourceCode) | ||
} | ||
println("Created BundleLine enum class(es) for bundles in directory $parentDirectory:") | ||
println(enumNamesToEntryNames.keys.joinToString(separator = ",\n", prefix = " ")) | ||
println("in package $targetPackage in source directory $targetSourceDirectory.") | ||
} catch (e: IOException) { | ||
printlnRed("An IO error was encountered while executing BundleLinesCreator.") | ||
throw e | ||
} | ||
} | ||
|
||
/** Finds the parent directory that will be searched for properties files. The default implementation finds the first | ||
* non-null and existing directory in descending precedence of [assetDirectory], `android/assets/i18n`, | ||
* `android/assets/nls`, `android/assets`, `core/assets/i18n`, `core/assets/nls`, and `core/assets`. | ||
* @return The parent directory, or null if none exists. | ||
*/ | ||
protected open fun findParentDirectory(assetDirectory: String?): File? { | ||
return listOfNotNull( | ||
assetDirectory, | ||
"android/assets/i18n", | ||
"android/assets/nls", | ||
"android/assets/", | ||
"core/assets/i18n", | ||
"core/assets/nls", | ||
"core/assets" | ||
) | ||
.map(::File).firstOrNull(File::isDirectory) | ||
} | ||
|
||
/** Collects properties files that will be processed. The default implementation finds all files with name suffix | ||
* `.properties` and no underscore in the name. The [searchSubdirectories] | ||
* property determines whether subdirectories of [parentDirectory] are also included in the search. | ||
* @param parentDirectory The directory that will be searched. | ||
* @return A list of all applicable properties files found. | ||
*/ | ||
protected open fun findBasePropertiesFiles(parentDirectory: File, searchSubdirectories: Boolean): Collection<File> { | ||
val noUnderscoresFilter = object : AbstractFileFilter() { | ||
override fun accept(dir: File, name: String) = '_' !in name | ||
} | ||
|
||
return FileUtils.listFiles( | ||
parentDirectory, | ||
FileFilterUtils.and(SuffixFileFilter("properties"), noUnderscoresFilter), | ||
if (searchSubdirectories) TrueFileFilter.INSTANCE else null | ||
) | ||
} | ||
|
||
/** Takes the target [propertiesFiles] and returns a map of output enum class BundleLines to their set of String keys. | ||
* By default, if [commonBaseName] is non-null, a single base name is output and all property keys will be merged | ||
* into the single set. Otherwise, the properties files' names are used as base names. */ | ||
protected open fun collectKeys(propertiesFiles: Collection<File>, commonBaseName: String?): Map<String, Set<String>> { | ||
val outMap = mutableMapOf<String, MutableSet<String>>() | ||
val sanitizedCommonBaseName = commonBaseName?.let { sanitizeEnumName(it) } | ||
if (sanitizedCommonBaseName != commonBaseName) | ||
println("The provided enumClassName $commonBaseName was changed to $sanitizedCommonBaseName.") | ||
for (file in propertiesFiles) { | ||
val enumName = sanitizedCommonBaseName ?: sanitizeEnumName(file.nameWithoutExtension) | ||
val propertyNames = Properties().run { | ||
load(file.inputStream()) | ||
stringPropertyNames() | ||
} | ||
val enumNames = propertyNames.mapNotNull { sanitizeEntryName(it) } | ||
if (propertyNames.size > enumNames.size) | ||
printlnRed("Warning: Properties file ${file.name} contains at least one empty key, which will be omitted.") | ||
outMap.getOrPut(enumName, ::mutableSetOf) | ||
.addAll(enumNames) | ||
} | ||
return outMap | ||
} | ||
|
||
private fun sanitizeEnumName(name: String): String { | ||
return name.trimStart { !it.isLetterOrDigit() && it != '_' } | ||
.split("\\s+".toRegex()) | ||
.joinToString("", transform = String::capitalize) | ||
} | ||
|
||
/** @return A name based on [name] that is a valid enum entry name, or null if the input name is empty. */ | ||
private fun sanitizeEntryName(name: String): String? { | ||
if (name.isEmpty()) | ||
return null | ||
if (name[0].isDigit() || !name.all { it.isLetterOrDigit() || it == '_' }) | ||
return "`$name`" | ||
return name | ||
} | ||
|
||
protected open fun generateKtFileContent( | ||
packageName: String, | ||
enumClassName: String, | ||
entryNames: Set<String> | ||
): String { | ||
return buildString { | ||
append("@file:Suppress(\"EnumEntryName\")\n\n") | ||
append("package $packageName\n\n") | ||
append("import ktx.i18n.BundleLine\n") | ||
append("import com.badlogic.gdx.utils.I18NBundle\n") | ||
append("\nenum class $enumClassName: BundleLine {\n") | ||
for ((index, key) in entryNames.withIndex()) { | ||
val lineEnding = if (index < entryNames.size - 1) ',' else ';' | ||
append("${IND}$key$lineEnding\n") | ||
} | ||
append("\n${IND}override val bundle: I18NBundle\n") | ||
append("${IND2}get() = i18nBundle\n") | ||
append("\n${IND}companion object {\n") | ||
append("${IND2}/** The bundle used for [BundleLine.nls] and [BundleLine.invoke] for this enum's values. */\n") | ||
append("${IND2}lateinit var i18nBundle: I18NBundle\n") | ||
append("${IND}}\n") | ||
append('}') | ||
} | ||
} | ||
} | ||
|
||
private const val IND: String = " " | ||
private const val IND2: String = IND + IND |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
@file:Suppress("unused") | ||
|
||
package ktx.tools | ||
|
||
import org.gradle.api.Plugin | ||
import org.gradle.api.Project | ||
|
||
private const val EXTENSION_NAME = "ktxTools" | ||
|
||
class KtxToolsPlugin : Plugin<Project> { | ||
override fun apply(project: Project) { | ||
val ktxToolsExtension = project.extensions.create(EXTENSION_NAME, KtxToolsPluginExtension::class.java) | ||
|
||
project.tasks.create("createBundleLines") { task -> | ||
task.doLast { | ||
val ext = ktxToolsExtension.createBundleLines | ||
val targetPackage = ext.targetPackage | ||
if (targetPackage == null) { | ||
printlnRed("Cannot create BundleLines if target package is not set. This can be set in the gradle build file, e.g.:") | ||
printlnRed( | ||
"\n $EXTENSION_NAME.${KtxToolsPluginExtension::createBundleLines.name}" + | ||
".${CreateBundleLinesParams::targetPackage.name} = \"com.mycompany.mygame\"\n" | ||
) | ||
return@doLast | ||
} | ||
|
||
BundleLinesCreator.execute( | ||
targetPackage = targetPackage, | ||
explicitParentDirectory = ext.sourceParentDirectory, | ||
searchSubdirectories = ext.searchSubDirectories, | ||
targetSourceDirectory = ext.targetSourceDirectory, | ||
enumClassName = ext.enumClassName | ||
) | ||
} | ||
task.group = "ktx tools" | ||
} | ||
} | ||
} | ||
|
||
open class KtxToolsPluginExtension { | ||
var createBundleLines = CreateBundleLinesParams() | ||
} | ||
|
||
/** Parameters for the `createBundleLines` task. */ | ||
open class CreateBundleLinesParams { | ||
/** The package created enums will be placed in. If null (the default), the `createBundleLines` task cannot be used. */ | ||
var targetPackage: String? = null | ||
|
||
/** Directory that is searched for properties files when creating BundleLines. If null (the default), the first | ||
* non-null and existing directory in descending precedence of `android/assets/i18n`, `android/assets/nls`, | ||
* `android/assets`, `core/assets/i18n`, `core/assets/nls`, and `core/assets` is used. */ | ||
var sourceParentDirectory: String? = null | ||
|
||
/** Whether to search subdirectories of the parent directory. Default true. */ | ||
var searchSubDirectories: Boolean = true | ||
|
||
/** The directory enum source files will be placed in. Default `"core/src"`. */ | ||
var targetSourceDirectory: String = "core/src" | ||
|
||
/** The name of the generated enum class. If non-null, a single enum class is created for all bundles found. If null, | ||
* each unique base bundle's name will be used for a distinct enum class. Default `"Nls"`.*/ | ||
var enumClassName: String? = "Nls" | ||
} | ||
|
||
internal fun printlnRed(message: String) = println("\u001B[0;31m${message}\u001B[0m") |