diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 14700a2..cda804b 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,38 +1,37 @@ -name-template: '$RESOLVED_VERSION' -tag-template: '$RESOLVED_VERSION' +--- +name-template: "$RESOLVED_VERSION" +tag-template: "$RESOLVED_VERSION" prerelease: true template: | # What's Changed $CHANGES categories: - - title: Breaking - label: breaking - - title: New - label: enhancement - - title: Bug Fixes - label: bug - - title: Maintenance - label: maintenance - - title: Documentation - label: documentation - - title: Dependency Updates - label: dependencies - +- title: "Breaking" + label: "breaking" +- title: "New" + label: "enhancement" +- title: "Bug Fixes" + label: "bug" +- title: "Maintenance" + label: "maintenance" +- title: "Documentation" + label: "documentation" +- title: "Dependency Updates" + label: "dependencies" version-resolver: major: labels: - - breaking + - "breaking" minor: labels: - - enhancement + - "enhancement" patch: labels: - - bug - - maintenance - - documentation - - dependencies - - security - + - "bug" + - "maintenance" + - "documentation" + - "dependencies" + - "security" exclude-labels: - - skip-changelog +- "skip-changelog" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3881a9..1a7bcea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,35 +1,31 @@ -name: Build - -on: +--- +name: "Build" +"on": pull_request: - branches: [ master ] - + branches: + - "master" jobs: build: - name: Build - runs-on: ubuntu-latest + name: "Build" + runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: '11' - - - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-cache-${{ hashFiles('**/*.gradle.kts') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - uses: actions/cache@v4 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradlew- - - - run: | - chmod +x gradlew - ./gradlew build + - uses: "actions/checkout@v4" + - uses: "actions/setup-java@v4" + with: + distribution: "adopt" + java-version: "11" + - uses: "actions/cache@v4" + with: + path: "~/.gradle/caches" + key: "${{ runner.os }}-gradle-cache-${{ hashFiles('**/*.gradle.kts') }}" + restore-keys: | + ${{ runner.os }}-gradle- + - uses: "actions/cache@v4" + with: + path: "~/.gradle/wrapper" + key: "${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }}" + restore-keys: | + ${{ runner.os }}-gradlew- + - run: | + chmod +x gradlew + ./gradlew build diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index fef8afa..1e80163 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,16 +1,16 @@ -name: Update Changelog - -on: +--- +name: "Update Changelog" +"on": push: - branches: [ master ] - + branches: + - "master" jobs: changelog: - name: Update Changelog - runs-on: ubuntu-latest - if: ${{ !contains(github.event.head_commit.message, 'skip-snapshot') }} + name: "Update Changelog" + runs-on: "ubuntu-latest" + if: "${{ !contains(github.event.head_commit.message, 'skip-snapshot') }}" steps: - - uses: release-drafter/release-drafter@master - id: release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: "release-drafter/release-drafter@master" + id: "release" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ed5670..0e5a0f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,35 +1,32 @@ -name: Release -on: +--- +name: "Release" +"on": release: types: - - released + - "released" jobs: build: - name: Release - runs-on: ubuntu-latest + name: "Release" + runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: '11' - - - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-cache-${{ hashFiles('**/*.gradle.kts') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - uses: actions/cache@v4 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradlew- - - - run: | - [[ "${{ github.event.release.tag_name }}" =~ ^[0-9]+(\.[0-9]+)*$ ]] || exit -1 - chmod +x gradlew - ./gradlew -Psign-required=true publish -Pversion="${{ github.event.release.tag_name }}" -PmavenCentralUsername="${{ secrets.MAVEN_CENTRAL_USERNAME }}" -PmavenCentralPassword="${{ secrets.MAVEN_CENTRAL_PASSWORD }}" -PsigningInMemoryKey="${{ secrets.GPG_PRIVATE_KEY_ARMORED }}" -PsigningInMemoryKeyPassword="${{ secrets.GPG_PASSPHRASE }}" + - uses: "actions/checkout@v4" + - uses: "actions/setup-java@v4" + with: + distribution: "adopt" + java-version: "11" + - uses: "actions/cache@v4" + with: + path: "~/.gradle/caches" + key: "${{ runner.os }}-gradle-cache-${{ hashFiles('**/*.gradle.kts') }}" + restore-keys: | + ${{ runner.os }}-gradle- + - uses: "actions/cache@v4" + with: + path: "~/.gradle/wrapper" + key: "${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }}" + restore-keys: | + ${{ runner.os }}-gradlew- + - run: | + [[ "${{ github.event.release.tag_name }}" =~ ^[0-9]+(\.[0-9]+)*$ ]] || exit -1 + chmod +x gradlew + ./gradlew -Psign-required=true publish -Pversion="${{ github.event.release.tag_name }}" -PmavenCentralUsername="${{ secrets.MAVEN_CENTRAL_USERNAME }}" -PmavenCentralPassword="${{ secrets.MAVEN_CENTRAL_PASSWORD }}" -PsigningInMemoryKey="${{ secrets.GPG_PRIVATE_KEY_ARMORED }}" -PsigningInMemoryKeyPassword="${{ secrets.GPG_PASSPHRASE }}" diff --git a/README.md b/README.md index 1418ec7..3ea8abb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,25 @@ repositories { dependencies { // Base module implementation "net.infumia:pack:VERSION" + // Required, https://mvnrepository.com/artifact/net.kyori/adventure-api/ + implementation "net.kyori:adventure-api:4.17.0" + // Required, https://mvnrepository.com/artifact/team.unnamed/creative-api/ + implementation "team.unnamed:creative-api:1.7.2" + + // Blank Slot (Optional) + implementation "net.infumia:pack-blank:VERSION" + + // Language (Optional) + implementation "net.infumia:pack-language:VERSION" + + // Generator (Optional) + implementation "net.infumia:pack-generator:VERSION" + // Required, https://mvnrepository.com/artifact/team.unnamed/team.unnamed:creative-serializer-minecraft/ + implementation "team.unnamed:team.unnamed:creative-serializer-minecraft:1.7.2" + // Required, https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind/ + implementation "com.fasterxml.jackson.core:jackson-databind:" + // Required, https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/ + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:" } ``` ### Code diff --git a/blank/build.gradle.kts b/blank/build.gradle.kts new file mode 100644 index 0000000..05d7dad --- /dev/null +++ b/blank/build.gradle.kts @@ -0,0 +1,9 @@ +import net.infumia.gradle.publish + +publish("blank") + +dependencies { + compileOnly(project(":common")) + + compileOnly(libs.creative.api) +} diff --git a/blank/src/main/java/net/infumia/pack/BlankSlot.java b/blank/src/main/java/net/infumia/pack/BlankSlot.java new file mode 100644 index 0000000..f50daf8 --- /dev/null +++ b/blank/src/main/java/net/infumia/pack/BlankSlot.java @@ -0,0 +1,63 @@ +package net.infumia.pack; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.KeyPattern; +import team.unnamed.creative.base.Writable; + +/** + * Utility class for creating and retrieving blank slot file resources. + */ +public final class BlankSlot { + + /** + * Retrieves a pre-configured blank slot file resource. + * + * @return A {@link FileResource} representing the blank slot. + */ + public static FileResource get() { + return Internal0.BLANK_SLOT.get(); + } + + /** + * Creates a blank slot file resource with the specified parameters. + * + * @param namespace The namespace for the model. Cannot be null. + * @param itemId The id for the item. Cannot be null. + * @param baseKey The key for the base model. Cannot be null. + * @param blankSlotImage The writable image for the blank slot. Cannot be null. + * @param customModelData The custom model data value. + * @return A {@link FileResource} representing the created blank slot. + */ + public static FileResource create( + @KeyPattern.Namespace final String namespace, + @KeyPattern.Value final String itemId, + final Key baseKey, + final Writable blankSlotImage, + final int customModelData + ) { + return ResourceProducers.item( + Key.key(namespace, itemId), + baseKey, + blankSlotImage, + customModelData + ); + } + + private BlankSlot() { + throw new IllegalStateException("Utility class"); + } + + private static final class Internal0 { + + private static final Lazy BLANK_SLOT = Lazy.of( + () -> + BlankSlot.create( + Internal.DEFAULT_NAMESPACE, + "blank_slot", + Key.key("item/paper"), + Internal.resourceFromJar("blank_slot.png"), + 1 + ) + ); + } +} diff --git a/blank/src/main/resources/pack-resources/blank_slot.png b/blank/src/main/resources/pack-resources/blank_slot.png new file mode 100644 index 0000000..0058d7a Binary files /dev/null and b/blank/src/main/resources/pack-resources/blank_slot.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 399137a..ff3dd55 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,73 +1,7 @@ -import com.vanniktech.maven.publish.MavenPublishBaseExtension -import com.vanniktech.maven.publish.MavenPublishPlugin -import com.vanniktech.maven.publish.SonatypeHost +import net.infumia.gradle.spotless -plugins { - java - alias(libs.plugins.nexus) apply false -} +plugins { java } -repositories.mavenCentral() +subprojects { apply() } -subprojects { - apply() - apply() - - repositories.mavenCentral() - - java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } - } - - tasks { - val javadocJar by creating(Jar::class) { - dependsOn("javadoc") - archiveClassifier.set("javadoc") - from(javadoc) - } - - val sourcesJar by creating(Jar::class) { - dependsOn("classes") - archiveClassifier.set("sources") - from(sourceSets["main"].allSource) - } - } - - val moduleName = project.findProperty("artifact-id") as String? - val projectName = "pack${if (moduleName == null) "" else "-$moduleName"}" - val signRequired = project.hasProperty("sign-required") - - extensions.configure { - coordinates(project.group.toString(), projectName, project.version.toString()) - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) - if (signRequired) { - signAllPublications() - } - - pom { - name.set(projectName) - description.set("Minecraft resource pack generator.") - url.set("https://github.com/Infumia/pack") - licenses { - license { - name.set("MIT License") - url.set("https://mit-license.org/license.txt") - } - } - developers { - developer { - id.set("portlek") - name.set("Hasan Demirtaş") - email.set("utsukushihito@outlook.com") - } - } - scm { - connection.set("scm:git:git://github.com/infumia/pack.git") - developerConnection.set("scm:git:ssh://github.com/infumia/pack.git") - url.set("https://github.com/infumia/pack/") - } - } - } -} +spotless() diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..910a66b --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { `kotlin-dsl` } + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation(libs.nexus.plugin) + implementation(libs.spotless.plugin) +} + +kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of(11) + vendor = JvmVendorSpec.ADOPTIUM + } +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..c8d9270 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { id("org.gradle.toolchains.foojay-resolver-convention") } + +dependencyResolutionManagement { + versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } +} diff --git a/buildSrc/src/main/kotlin/net/infumia/gradle/common.kt b/buildSrc/src/main/kotlin/net/infumia/gradle/common.kt new file mode 100644 index 0000000..7ef5b89 --- /dev/null +++ b/buildSrc/src/main/kotlin/net/infumia/gradle/common.kt @@ -0,0 +1,41 @@ +package net.infumia.gradle + +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.bundling.Jar +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JvmVendorSpec +import org.gradle.kotlin.dsl.* + +fun Project.applyCommon(javaVersion: Int = 8, sources: Boolean = true, javadoc: Boolean = true) { + apply() + + repositories.mavenCentral() + + extensions.configure { + toolchain { + languageVersion = JavaLanguageVersion.of(javaVersion) + vendor = JvmVendorSpec.ADOPTIUM + } + } + + if (javadoc) { + val javadocJar by + tasks.creating(Jar::class) { + dependsOn("javadoc") + archiveClassifier.set("javadoc") + from(javadoc) + } + } + + if (sources) { + val sourceSets = extensions.getByType().sourceSets + val sourcesJar by + tasks.creating(Jar::class) { + dependsOn("classes") + archiveClassifier.set("sources") + from(sourceSets["main"].allSource) + } + } +} diff --git a/buildSrc/src/main/kotlin/net/infumia/gradle/publish.kt b/buildSrc/src/main/kotlin/net/infumia/gradle/publish.kt new file mode 100644 index 0000000..25b2cc8 --- /dev/null +++ b/buildSrc/src/main/kotlin/net/infumia/gradle/publish.kt @@ -0,0 +1,52 @@ +package net.infumia.gradle + +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.MavenPublishPlugin +import com.vanniktech.maven.publish.SonatypeHost +import org.gradle.api.Project +import org.gradle.kotlin.dsl.* + +fun Project.publish( + moduleName: String? = null, + javaVersion: Int = 8, + sources: Boolean = true, + javadoc: Boolean = true +) { + applyCommon(javaVersion, sources, javadoc) + apply() + + val projectName = "pack${if (moduleName == null) "" else "-$moduleName"}" + val signRequired = project.hasProperty("sign-required") + + extensions.configure { + coordinates(project.group.toString(), projectName, project.version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) + if (signRequired) { + signAllPublications() + } + + pom { + name.set(projectName) + description.set("Minecraft resource pack generator.") + url.set("https://github.com/Infumia/pack") + licenses { + license { + name.set("MIT License") + url.set("https://mit-license.org/license.txt") + } + } + developers { + developer { + id.set("portlek") + name.set("Hasan Demirtaş") + email.set("utsukushihito@outlook.com") + } + } + scm { + connection.set("scm:git:git://github.com/infumia/pack.git") + developerConnection.set("scm:git:ssh://github.com/infumia/pack.git") + url.set("https://github.com/infumia/pack/") + } + } + } +} diff --git a/buildSrc/src/main/kotlin/net/infumia/gradle/spotless.kt b/buildSrc/src/main/kotlin/net/infumia/gradle/spotless.kt new file mode 100644 index 0000000..95553c9 --- /dev/null +++ b/buildSrc/src/main/kotlin/net/infumia/gradle/spotless.kt @@ -0,0 +1,91 @@ +package net.infumia.gradle + +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessPlugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.* + +fun Project.spotless() { + val subProjects = subprojects.map { it.projectDir.toRelativeString(projectDir) } + + repositories.mavenCentral() + + apply() + + extensions.configure { + isEnforceCheck = false + lineEndings = com.diffplug.spotless.LineEnding.UNIX + + val prettierConfig = + mapOf( + "prettier" to "3.3.2", + "prettier-plugin-java" to "2.6.0", + "prettier-plugin-toml" to "2.0.1", + ) + + yaml { + target(".github/**/*.yml") + endWithNewline() + trimTrailingWhitespace() + jackson().yamlFeature("LITERAL_BLOCK_STYLE", true).yamlFeature("SPLIT_LINES", false) + } + + json { + target("renovate.json") + endWithNewline() + trimTrailingWhitespace() + jackson() + } + + format("toml") { + target("gradle/libs.versions.toml") + endWithNewline() + trimTrailingWhitespace() + prettier(prettierConfig) + .config( + mapOf( + "parser" to "toml", + "plugins" to listOf("prettier-plugin-toml"), + ), + ) + } + + kotlin { + target( + "buildSrc/src/main/kotlin/**/*.kt", + "buildSrc/**/*.gradle.kts", + "*.gradle.kts", + *subProjects.map { "$it/*.gradle.kts" }.toTypedArray(), + *subProjects.map { "$it/src/main/kotlin/**/*.kt" }.toTypedArray(), + ) + endWithNewline() + trimTrailingWhitespace() + ktfmt().kotlinlangStyle().configure { + it.setMaxWidth(100) + it.setBlockIndent(4) + it.setContinuationIndent(4) + it.setRemoveUnusedImport(true) + } + } + + java { + target( + *subProjects.map { "$it/src/main/java/**/*.java" }.toTypedArray(), + ) + importOrder() + removeUnusedImports() + endWithNewline() + trimTrailingWhitespace() + prettier(prettierConfig) + .config( + mapOf( + "parser" to "java", + "tabWidth" to 4, + "useTabs" to false, + "printWidth" to 100, + "plugins" to listOf("prettier-plugin-java"), + ), + ) + } + } +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..a9b4c70 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,5 @@ +import net.infumia.gradle.publish + +publish() + +dependencies { compileOnly(libs.creative.api) } diff --git a/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactory.java b/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactory.java new file mode 100644 index 0000000..c337cdc --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactory.java @@ -0,0 +1,14 @@ +package net.infumia.pack; + +/** + * Interface for a factory that creates arbitrary characters. + */ +public interface ArbitraryCharacterFactory { + /** + * Creates and returns an arbitrary character. + * + * @return a generated character. + * @throws IllegalStateException if the character range exceeds. + */ + char create() throws IllegalStateException; +} diff --git a/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactoryReserved.java b/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactoryReserved.java new file mode 100644 index 0000000..a0625a2 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ArbitraryCharacterFactoryReserved.java @@ -0,0 +1,120 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * A factory for creating arbitrary characters, ensuring that reserved characters are not produced. + */ +@SuppressWarnings("UnnecessaryUnicodeEscape") +public final class ArbitraryCharacterFactoryReserved implements ArbitraryCharacterFactory { + + private final Collection reserved; + private char startPoint; + + /** + * Constructs a new instance with the specified start point and collection of reserved characters. + * + * @param startPoint the starting character for generating new characters. + * @param reserved the collection of characters that should not be produced by the factory. Cannot be null. + */ + public ArbitraryCharacterFactoryReserved( + final char startPoint, + final Collection reserved + ) { + this.startPoint = startPoint; + this.reserved = reserved; + } + + /** + * Constructs a new instance with the specified collection of reserved characters, + * starting from the default character {@literal '\uA201'}. + * + * @param reserved the collection of characters that should not be produced by the factory. Cannot be null. + */ + public ArbitraryCharacterFactoryReserved(final Collection reserved) { + this('\uA201', reserved); + } + + /** + * Constructs a new instance with a default set of reserved characters, + * starting from the default character '\uA201'. + */ + public ArbitraryCharacterFactoryReserved() { + this(Internal.RESERVED.get()); + } + + @Override + public char create() throws IllegalStateException { + if (this.startPoint == Character.MAX_VALUE) { + throw new IllegalStateException("Characters range exceeded"); + } + do { + this.startPoint++; + } while (this.reserved.contains(this.startPoint)); + return this.startPoint; + } + + private static final class Internal { + + private static final Lazy> RESERVED = Lazy.of(() -> { + final Collection reserved = new ArrayList<>(); + for (char c = 'a'; c <= 'z'; c++) { + reserved.add(c); + } + for (char c = 'A'; c <= 'Z'; c++) { + reserved.add(c); + } + for (char c = '0'; c <= '9'; c++) { + reserved.add(c); + } + Collections.addAll( + reserved, + '!', + '?', + ':', + '$', + ';', + '#', + '@', + '%', + '^', + '&', + '*', + '(', + ')', + '_', + '-', + '+', + '/', + '\\', + '"', + '\'', + '{', + '}', + '[', + ']', + '~', + '`', + '<', + '>', + ',', + '.', + '|', + '\n', + '\r', + '\b', + '\f', + '\t', + ' ', + '=' + ); + return reserved; + }); + + private Internal() { + throw new IllegalStateException("Utility class"); + } + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResource.java b/common/src/main/java/net/infumia/pack/FileResource.java new file mode 100644 index 0000000..6ced467 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResource.java @@ -0,0 +1,15 @@ +package net.infumia.pack; + +import team.unnamed.creative.ResourcePack; + +/** + * Represents a file resource that can be written to a resource pack. + */ +public interface FileResource { + /** + * Writes the file resource to the specified resource pack. + * + * @param pack the resource pack to which the file resource will be written. Cannot be null. + */ + void write(ResourcePack pack); +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceAll.java b/common/src/main/java/net/infumia/pack/FileResourceAll.java new file mode 100644 index 0000000..8c77e83 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceAll.java @@ -0,0 +1,20 @@ +package net.infumia.pack; + +import java.util.Collection; +import team.unnamed.creative.ResourcePack; + +final class FileResourceAll implements FileResource { + + final Collection resources; + + FileResourceAll(final Collection resources) { + this.resources = resources; + } + + @Override + public void write(final ResourcePack pack) { + for (final FileResource resource : this.resources) { + resource.write(pack); + } + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceAtlas.java b/common/src/main/java/net/infumia/pack/FileResourceAtlas.java new file mode 100644 index 0000000..faacf41 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceAtlas.java @@ -0,0 +1,18 @@ +package net.infumia.pack; + +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.atlas.Atlas; + +final class FileResourceAtlas implements FileResource { + + final Atlas atlas; + + FileResourceAtlas(final Atlas atlas) { + this.atlas = atlas; + } + + @Override + public void write(final ResourcePack pack) { + pack.atlas(this.atlas); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceCompiler.java b/common/src/main/java/net/infumia/pack/FileResourceCompiler.java new file mode 100644 index 0000000..42e7c92 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceCompiler.java @@ -0,0 +1,16 @@ +package net.infumia.pack; + +import java.util.Collection; + +/** + * Interface representing a resource pack compiler that compiles resources from multiple producers into file resources. + */ +public interface FileResourceCompiler { + /** + * Compiles resources produced by the given collection of {@link ResourceProducer}s into {@link FileResource}s. + * + * @param producers the collection of resource producers. Cannot be null. + * @return an unmodifiable collection of compiled file resources. + */ + Collection compile(Collection producers); +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceCompilerDefault.java b/common/src/main/java/net/infumia/pack/FileResourceCompilerDefault.java new file mode 100644 index 0000000..466e01a --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceCompilerDefault.java @@ -0,0 +1,45 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.font.FontProvider; + +final class FileResourceCompilerDefault implements FileResourceCompiler { + + private final ArbitraryCharacterFactory characterFactory; + + FileResourceCompilerDefault(final ArbitraryCharacterFactory characterFactory) { + this.characterFactory = characterFactory; + } + + @Override + public Collection compile(final Collection producers) { + final Collection resources = new HashSet<>(); + final Collection fontKeys = producers + .stream() + .map(ResourceProducer::key) + .collect(Collectors.toSet()); + for (final Key fontKey : fontKeys) { + final List fontProviders = new ArrayList<>(); + for (final ResourceProducer producer : producers) { + if (producer.key().equals(fontKey)) { + producer.produce(this.characterFactory); + fontProviders.addAll(producer.fontProviders()); + final List textureResources = producer + .textures() + .stream() + .map(FileResources::texture) + .collect(Collectors.toList()); + resources.add(FileResources.all(textureResources)); + } + } + resources.add(FileResources.font(Font.font(fontKey, fontProviders))); + } + return resources; + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceCompilers.java b/common/src/main/java/net/infumia/pack/FileResourceCompilers.java new file mode 100644 index 0000000..e0a3935 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceCompilers.java @@ -0,0 +1,40 @@ +package net.infumia.pack; + +/** + * Utility class providing factory methods to create instances of {@link FileResourceCompiler}. + */ +public final class FileResourceCompilers { + + /** + * Creates a simple {@link FileResourceCompiler} instance with a custom character factory. + * + * @param characterFactory The character factory to be used in the compilation. Cannot be null. + * @return A newly created {@link FileResourceCompiler} instance configured with the provided character factory. + */ + public static FileResourceCompiler simple(final ArbitraryCharacterFactory characterFactory) { + return new FileResourceCompilerDefault(characterFactory); + } + + /** + * Creates a default {@link FileResourceCompiler} instance using a reserved character factory. + * + * @return A newly created {@link FileResourceCompiler} instance configured with the default character factory. + */ + public static FileResourceCompiler simple() { + return Internal.DEFAULT_COMPILER.get(); + } + + private FileResourceCompilers() { + throw new IllegalStateException("Utility class"); + } + + private static final class Internal { + + private static final Lazy CHARACTER_FACTORY = Lazy.of( + ArbitraryCharacterFactoryReserved::new + ); + private static final Lazy DEFAULT_COMPILER = Lazy.of( + () -> FileResourceCompilers.simple(Internal.CHARACTER_FACTORY.get()) + ); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceFont.java b/common/src/main/java/net/infumia/pack/FileResourceFont.java new file mode 100644 index 0000000..b3ca93c --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceFont.java @@ -0,0 +1,18 @@ +package net.infumia.pack; + +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.font.Font; + +final class FileResourceFont implements FileResource { + + final Font font; + + FileResourceFont(final Font font) { + this.font = font; + } + + @Override + public void write(final ResourcePack pack) { + pack.font(this.font); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceMerger.java b/common/src/main/java/net/infumia/pack/FileResourceMerger.java new file mode 100644 index 0000000..2374230 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceMerger.java @@ -0,0 +1,21 @@ +package net.infumia.pack; + +import java.util.Collection; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.model.Model; + +/** + * Represents a file resource merger that can merge a collection of file resources. + */ +public interface FileResourceMerger { + /** + * Merges a collection of {@link FileResources} into a unified resource. + *

+ * Tries to merge duplicate {@link Atlas}s and {@link Model}s. + * + * @param resources the collection of {@link FileResources} to merge. Cannot be null. + * + * @return the merged collection of {@link FileResource}s. Cannot be null. + */ + Collection merge(Collection resources); +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceMergerDefault.java b/common/src/main/java/net/infumia/pack/FileResourceMergerDefault.java new file mode 100644 index 0000000..1a56f79 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceMergerDefault.java @@ -0,0 +1,118 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Collectors; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.model.ItemOverride; +import team.unnamed.creative.model.ItemPredicate; +import team.unnamed.creative.model.Model; + +final class FileResourceMergerDefault implements FileResourceMerger { + + static final FileResourceMerger INSTANCE = new FileResourceMergerDefault(); + static final Comparator OVERRIDE_COMPARATOR = Comparator.comparingInt(value -> { + final Optional first = value.predicate().stream().findFirst(); + if (!first.isPresent()) { + return 0; + } + final ItemPredicate predicate = first.get(); + if (predicate.name().equals("custom_model_data")) { + return (int) predicate.value(); + } + return 0; + }); + + private FileResourceMergerDefault() {} + + @Override + public Collection merge(final Collection resources) { + final Collection simplified = new ArrayList<>(resources.size()); + for (final FileResource resource : resources) { + simplified.addAll(this.simplify(resource)); + } + + final MultiMap atlases = new MultiMap<>(); + final MultiMap models = new MultiMap<>(); + final Collection remaining = new ArrayList<>(); + for (final FileResource resource : simplified) { + if (resource instanceof FileResourceAtlas) { + final Atlas atlas = ((FileResourceAtlas) resource).atlas; + atlases.put(atlas.key(), atlas); + } else if (resource instanceof FileResourceModel) { + final Model model = ((FileResourceModel) resource).model; + models.put(model.key(), model); + } + // TODO: portlek, Merge more things. + else { + remaining.add(resource); + } + } + + final Collection mergedAtlases = new ArrayList<>(atlases.keys().size()); + for (final Key key : atlases.keys()) { + final Collection duplicates = atlases.get(key); + final Atlas merged = Atlas.atlas() + .key(key) + .sources( + duplicates + .stream() + .map(Atlas::sources) + .flatMap(Collection::stream) + .collect(Collectors.toList()) + ) + .build(); + mergedAtlases.add(merged); + } + + final Collection mergedModels = new ArrayList<>(models.keys().size()); + for (final Key key : models.keys()) { + final Collection duplicates = models.get(key); + final Model.Builder builder = Model.model().key(key); + // TODO: portlek, Find a way to merge these too. + for (final Model duplicate : duplicates) { + builder + .guiLight(duplicate.guiLight()) + .elements(duplicate.elements()) + .parent(duplicate.parent()) + .ambientOcclusion(duplicate.ambientOcclusion()) + .display(duplicate.display()) + .textures(duplicate.textures()); + } + builder.overrides( + duplicates + .stream() + .map(Model::overrides) + .flatMap(Collection::stream) + .sorted(FileResourceMergerDefault.OVERRIDE_COMPARATOR) + .collect(Collectors.toList()) + ); + mergedModels.add(builder.build()); + } + + final Collection mergedResources = mergedAtlases + .stream() + .map(FileResources::atlas) + .collect(Collectors.toList()); + mergedResources.addAll( + mergedModels.stream().map(FileResources::model).collect(Collectors.toList()) + ); + mergedResources.addAll(remaining); + return mergedResources; + } + + private Collection simplify(final FileResource resource) { + if (resource instanceof FileResourceAll) { + return ((FileResourceAll) resource).resources.stream() + .map(this::simplify) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } else { + return Collections.singletonList(resource); + } + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceMergers.java b/common/src/main/java/net/infumia/pack/FileResourceMergers.java new file mode 100644 index 0000000..d60d6ad --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceMergers.java @@ -0,0 +1,20 @@ +package net.infumia.pack; + +/** + * Utility class for providing various implementations of {@link FileResourceMerger}. + */ +public final class FileResourceMergers { + + /** + * Returns a simple file resource merger implementation. + * + * @return A {@link FileResourceMerger} representing the simple file resource merger. + */ + public static FileResourceMerger simple() { + return FileResourceMergerDefault.INSTANCE; + } + + private FileResourceMergers() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceModel.java b/common/src/main/java/net/infumia/pack/FileResourceModel.java new file mode 100644 index 0000000..6682de9 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceModel.java @@ -0,0 +1,18 @@ +package net.infumia.pack; + +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.model.Model; + +final class FileResourceModel implements FileResource { + + final Model model; + + FileResourceModel(final Model model) { + this.model = model; + } + + @Override + public void write(final ResourcePack pack) { + pack.model(this.model); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResourceTexture.java b/common/src/main/java/net/infumia/pack/FileResourceTexture.java new file mode 100644 index 0000000..2aa828b --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResourceTexture.java @@ -0,0 +1,18 @@ +package net.infumia.pack; + +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.texture.Texture; + +final class FileResourceTexture implements FileResource { + + final Texture texture; + + FileResourceTexture(final Texture texture) { + this.texture = texture; + } + + @Override + public void write(final ResourcePack pack) { + pack.texture(this.texture); + } +} diff --git a/common/src/main/java/net/infumia/pack/FileResources.java b/common/src/main/java/net/infumia/pack/FileResources.java new file mode 100644 index 0000000..5cc6bb4 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/FileResources.java @@ -0,0 +1,78 @@ +package net.infumia.pack; + +import java.util.Arrays; +import java.util.Collection; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.model.Model; +import team.unnamed.creative.texture.Texture; + +/** + * Utility class for creating various types of {@link FileResource} instances. + */ +public final class FileResources { + + /** + * Creates a {@link FileResource} for the specified texture. + * + * @param texture the texture to create a file resource for. Cannot be null. + * @return a {@link FileResource} representing the texture. + */ + public static FileResource texture(final Texture texture) { + return new FileResourceTexture(texture); + } + + /** + * Creates a {@link FileResource} for the specified atlas. + * + * @param atlas the atlas to create a file resource for. Cannot be null. + * @return a {@link FileResource} representing the atlas. + */ + public static FileResource atlas(final Atlas atlas) { + return new FileResourceAtlas(atlas); + } + + /** + * Creates a {@link FileResource} for the specified font. + * + * @param font the font to create a file resource for. Cannot be null. + * @return a {@link FileResource} representing the font. + */ + public static FileResource font(final Font font) { + return new FileResourceFont(font); + } + + /** + * Creates a {@link FileResource} for the specified model. + * + * @param model the model to create a file resource for. Cannot be null. + * @return a {@link FileResource} representing the model. + */ + public static FileResource model(final Model model) { + return new FileResourceModel(model); + } + + /** + * Creates a {@link FileResource} for a collection of file resources. + * + * @param resources the collection of resources to create file resources for. Cannot be null. + * @return a {@link FileResource} representing all the resources. + */ + public static FileResource all(final Collection resources) { + return new FileResourceAll(resources); + } + + /** + * Creates a {@link FileResource} for a collection of file resources. + * + * @param resources the collection of resources to create file resources for. Cannot be null. + * @return a {@link FileResource} representing all the resources. + */ + public static FileResource all(final FileResource... resources) { + return FileResources.all(Arrays.asList(resources)); + } + + private FileResources() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/Glyph.java b/common/src/main/java/net/infumia/pack/Glyph.java new file mode 100644 index 0000000..d6b6414 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Glyph.java @@ -0,0 +1,24 @@ +package net.infumia.pack; + +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.text.Component; + +/** + * Represents a glyph that can be converted to an adventure component and provides width information. + */ +public interface Glyph { + /** + * Converts the glyph into an adventure component. + * + * @return The adventure component representing this glyph. + * @throws ResourceNotProducedException if the glyph cannot be converted to an adventure component. + */ + Component toAdventure() throws ResourceNotProducedException; + + /** + * Returns the width of the glyph. + * + * @return The width of the glyph as an integer. + */ + int width(); +} diff --git a/common/src/main/java/net/infumia/pack/GlyphAppendable.java b/common/src/main/java/net/infumia/pack/GlyphAppendable.java new file mode 100644 index 0000000..e54cb4f --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphAppendable.java @@ -0,0 +1,6 @@ +package net.infumia.pack; + +/** + * Serves to mark classes that are both Glyphs and can be appended. + */ +public interface GlyphAppendable extends Glyph {} diff --git a/common/src/main/java/net/infumia/pack/GlyphColorable.java b/common/src/main/java/net/infumia/pack/GlyphColorable.java new file mode 100644 index 0000000..13a9f98 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphColorable.java @@ -0,0 +1,22 @@ +package net.infumia.pack; + +import net.kyori.adventure.text.format.TextColor; + +/** + * Represents an interface for objects that can be colored with text color. + */ +public interface GlyphColorable { + /** + * Retrieves the current text color of the object. + * + * @return The current text color. + */ + TextColor color(); + + /** + * Updates the text color of the object. + * + * @param color The new text color to set. + */ + void updateColor(TextColor color); +} diff --git a/common/src/main/java/net/infumia/pack/GlyphComponentBuilder.java b/common/src/main/java/net/infumia/pack/GlyphComponentBuilder.java new file mode 100644 index 0000000..a375a0d --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphComponentBuilder.java @@ -0,0 +1,172 @@ +package net.infumia.pack; + +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +/** + * Interface for building glyph components with various positioning and appending capabilities. + */ +public interface GlyphComponentBuilder { + /** + * Creates a universal GlyphComponentBuilder with the specified space producer. + * + * @param spacesProducer The space producer to use. + * @return A new GlyphComponentBuilder instance. + */ + static GlyphComponentBuilder universal(final ResourceProducerSpaces spacesProducer) { + return new GlyphComponentBuilderImpl(spacesProducer, 0, Component.text("")); + } + + /** + * Creates a GUI GlyphComponentBuilder with the specified space producer. + * + * @param spacesProducer The space producer to use. + * @return A new GlyphComponentBuilder instance. + */ + static GlyphComponentBuilder gui(final ResourceProducerSpaces spacesProducer) { + return new GlyphComponentBuilderImpl( + spacesProducer, + -8, + Component.text("", NamedTextColor.WHITE) + ); + } + + /** + * Creates a custom GlyphComponentBuilder with the specified parameters. + * + * @param spacesProducer The space producer to use. + * @param position The initial position. + * @param baseComponent The base component. + * @return A new GlyphComponentBuilder instance. + */ + static GlyphComponentBuilder custom( + final ResourceProducerSpaces spacesProducer, + final int position, + final Component baseComponent + ) { + return new GlyphComponentBuilderImpl(spacesProducer, position, baseComponent); + } + + /** + * Appends a glyph with a specified position type and position. + * + * @param positionType The type of position (absolute or relative). + * @param position The position value. + * @param glyph The glyph to append. + * @return This GlyphComponentBuilder instance. + */ + GlyphComponentBuilder append(PositionType positionType, int position, GlyphAppendable glyph); + + /** + * Appends a glyph with specified position type and default position (0). + * + * @param positionType The type of position (absolute or relative). + * @param glyph The glyph to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append( + final PositionType positionType, + final GlyphAppendable glyph + ) { + return this.append(positionType, 0, glyph); + } + + /** + * Appends a list of glyphs with a specified position type and position. + * + * @param positionType The type of position (absolute or relative). + * @param position The position value. + * @param glyphes The list of glyphs to append. + * @return This GlyphComponentBuilder instance. + */ + GlyphComponentBuilder append( + PositionType positionType, + int position, + List glyphes + ); + + /** + * Appends a list of glyphs with specified position type and default position (0). + * + * @param positionType The type of position (absolute or relative). + * @param glyphList The list of glyphs to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append( + final PositionType positionType, + final List glyphList + ) { + return this.append(positionType, 0, glyphList); + } + + /** + * Appends a glyph with an absolute position type and specified position. + * + * @param position The position value. + * @param glyph The glyph to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append(final int position, final GlyphAppendable glyph) { + return this.append(PositionType.ABSOLUTE, position, glyph); + } + + /** + * Appends a glyph with absolute position type and default position. + * + * @param glyph The glyph to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append(final GlyphAppendable glyph) { + return this.append(PositionType.ABSOLUTE, glyph); + } + + /** + * Appends a list of glyphs with an absolute position type and specified position. + * + * @param position The position value. + * @param glyphList The list of glyphs to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append( + final int position, + final List glyphList + ) { + return this.append(PositionType.ABSOLUTE, position, glyphList); + } + + /** + * Appends a list of glyphs with absolute position type and default position. + * + * @param glyphList The list of glyphs to append. + * @return This GlyphComponentBuilder instance. + */ + default GlyphComponentBuilder append(final List glyphList) { + return this.append(PositionType.ABSOLUTE, glyphList); + } + + /** + * Builds the component with an option to keep the initial position. + * + * @param keepInitialPosition Whether to keep the initial position. + * @return The built Component. + */ + Component build(boolean keepInitialPosition); + + /** + * Builds the component keeping the initial position. + * + * @return The built Component. + */ + default Component build() { + return this.build(true); + } + + /** + * Enum representing the type of position for appending glyphs. + */ + enum PositionType { + ABSOLUTE, + RELATIVE, + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphComponentBuilderImpl.java b/common/src/main/java/net/infumia/pack/GlyphComponentBuilderImpl.java new file mode 100644 index 0000000..423b8b8 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphComponentBuilderImpl.java @@ -0,0 +1,86 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.text.Component; + +final class GlyphComponentBuilderImpl implements GlyphComponentBuilder { + + private final ResourceProducerSpaces spacesProducer; + private final int initialPosition; + private final Component baseComponent; + + private final List glyphs = new ArrayList<>(); + + private int previousElementsWidth; + + public GlyphComponentBuilderImpl( + final ResourceProducerSpaces spacesProducer, + final int initialPosition, + final Component baseComponent + ) { + this.spacesProducer = spacesProducer; + this.initialPosition = initialPosition; + this.baseComponent = baseComponent; + } + + @Override + public GlyphComponentBuilder append( + final PositionType positionType, + final int position, + final GlyphAppendable glyph + ) { + this.preAppend(positionType, position); + + this.glyphs.add(glyph); + this.previousElementsWidth += position + glyph.width(); + + return this; + } + + @Override + public GlyphComponentBuilder append( + final PositionType positionType, + final int position, + final List glyphList + ) { + this.preAppend(positionType, position); + int width = 0; + for (final GlyphAppendable glyph : glyphList) { + this.glyphs.add(glyph); + width += glyph.width(); + } + this.previousElementsWidth += position + width; + return this; + } + + @Override + public Component build(final boolean keepInitialPosition) { + if (keepInitialPosition) { + this.previousElementsWidth += this.initialPosition; + if (this.previousElementsWidth != 0) { + this.glyphs.add(this.spacesProducer.translate((-1) * this.previousElementsWidth)); + } + } + Component component = this.baseComponent; + if (this.initialPosition != 0) { + component = component.append( + this.spacesProducer.translate(this.initialPosition).toAdventure() + ); + } + for (final Glyph glyph : this.glyphs) { + component = component.append(glyph.toAdventure()); + } + return component; + } + + private void preAppend(final PositionType positionType, final int position) { + if (positionType == PositionType.ABSOLUTE && this.previousElementsWidth != 0) { + this.glyphs.add(this.spacesProducer.translate((-1) * this.previousElementsWidth)); + this.previousElementsWidth = 0; + } + if (position != 0) { + this.glyphs.add(this.spacesProducer.translate(position)); + } + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphEmpty.java b/common/src/main/java/net/infumia/pack/GlyphEmpty.java new file mode 100644 index 0000000..b21ee5e --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphEmpty.java @@ -0,0 +1,21 @@ +package net.infumia.pack; + +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.text.Component; + +final class GlyphEmpty implements GlyphAppendable { + + static final GlyphEmpty INSTANCE = new GlyphEmpty(); + + private GlyphEmpty() {} + + @Override + public Component toAdventure() throws ResourceNotProducedException { + return Component.text(""); + } + + @Override + public int width() { + return 0; + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImage.java b/common/src/main/java/net/infumia/pack/GlyphImage.java new file mode 100644 index 0000000..0884245 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImage.java @@ -0,0 +1,46 @@ +package net.infumia.pack; + +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import team.unnamed.creative.texture.Texture; + +/** + * Represents an interface for a glyph image that can be appended and serves as a resource producer. + */ +public interface GlyphImage extends GlyphAppendable, ResourceProducer { + /** + * Retrieves the character associated with this glyph image. + * + * @return The character of the glyph image. + * @throws ResourceNotProducedException If the glyph image is not produced. + */ + char character() throws ResourceNotProducedException; + + /** + * Retrieves the texture associated with this glyph image. + * + * @return The texture of the glyph image. + */ + Texture texture(); + + /** + * Creates a colored version of this glyph image with the specified text color. + * + * @param color The text color for the colored glyph image. Cannot be null. + * @return A colored GlyphImageColored instance. + */ + default GlyphImageColored withColor(final TextColor color) { + return new GlyphImageColoredImpl(this, color); + } + + /** + * Converts this glyph image to an Adventure Component. + * + * @return The Adventure Component representing this glyph image. + * @throws ResourceNotProducedException If the glyph image is not produced. + */ + default Component toAdventure() throws ResourceNotProducedException { + return Component.text(this.character()).font(this.key()); + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImageColored.java b/common/src/main/java/net/infumia/pack/GlyphImageColored.java new file mode 100644 index 0000000..df8c2ce --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImageColored.java @@ -0,0 +1,13 @@ +package net.infumia.pack; + +/** + * Represents an interface for a colored glyph image that can be appended and has color attributes. + */ +public interface GlyphImageColored extends GlyphAppendable, GlyphColorable { + /** + * Retrieves the original glyph image associated with this colored glyph image. + * + * @return The original glyph image. + */ + GlyphImage original(); +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImageColoredImpl.java b/common/src/main/java/net/infumia/pack/GlyphImageColoredImpl.java new file mode 100644 index 0000000..586ee1f --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImageColoredImpl.java @@ -0,0 +1,45 @@ +package net.infumia.pack; + +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; + +final class GlyphImageColoredImpl implements GlyphImageColored { + + private final GlyphImage original; + private TextColor color; + + GlyphImageColoredImpl(final GlyphImage original, final TextColor color) { + this.original = original; + this.color = color; + } + + @Override + public Component toAdventure() throws ResourceNotProducedException { + Component component = this.original.toAdventure(); + if (this.color != null) { + component = component.color(this.color); + } + return component; + } + + @Override + public int width() { + return this.original.width(); + } + + @Override + public TextColor color() { + return this.color; + } + + @Override + public void updateColor(final TextColor color) { + this.color = color; + } + + @Override + public GlyphImage original() { + return this.original; + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImageImpl.java b/common/src/main/java/net/infumia/pack/GlyphImageImpl.java new file mode 100644 index 0000000..388f443 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImageImpl.java @@ -0,0 +1,107 @@ +package net.infumia.pack; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.imageio.ImageIO; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; +import team.unnamed.creative.font.BitMapFontProvider; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +final class GlyphImageImpl implements GlyphImage { + + private final Key key; + private final Texture texture; + private final TextureProperties properties; + + private Character character; + private List fontProviders; + + private int width = -1; + + GlyphImageImpl(final Key key, final Texture texture, final TextureProperties properties) { + this.key = key; + this.texture = texture; + this.properties = properties; + } + + @NotNull + @Override + public Key key() { + return this.key; + } + + @Override + public boolean produced() { + return this.fontProviders != null; + } + + @Override + public void produce(final ArbitraryCharacterFactory characterFactory) + throws ResourceAlreadyProducedException { + if (this.fontProviders != null || this.character != null) { + throw new ResourceAlreadyProducedException(); + } + this.character = characterFactory.create(); + final BitMapFontProvider.Builder fontProviderBuilder = FontProvider.bitMap(); + fontProviderBuilder.characters(String.valueOf(this.character)); + fontProviderBuilder.file(this.texture.key()); + fontProviderBuilder.ascent(this.properties.ascent()); + fontProviderBuilder.height(this.properties.height()); + this.fontProviders = Collections.singletonList(fontProviderBuilder.build()); + } + + @Override + public Collection fontProviders() throws ResourceNotProducedException { + if (this.fontProviders == null) { + throw new ResourceNotProducedException(); + } + return this.fontProviders; + } + + @Override + public Collection textures() throws ResourceNotProducedException { + return Collections.singletonList(this.texture); + } + + @Override + public int width() { + if (this.width != -1) { + return this.width; + } + try { + final BufferedImage image = ImageIO.read( + new ByteArrayInputStream(this.texture.data().toByteArray()) + ); + final int fileHeight = image.getHeight(); + this.width = (int) Math.ceil( + ((double) this.properties.height() / (double) fileHeight) * + Internal.calculateWidth(image) + ) + + Internal.SEPARATOR_WIDTH; + } catch (final IOException e) { + throw new RuntimeException(e); + } + return this.width; + } + + @Override + public char character() throws ResourceNotProducedException { + if (this.character == null) { + throw new ResourceNotProducedException(); + } + return this.character; + } + + @Override + public Texture texture() { + return this.texture; + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImagePrepared.java b/common/src/main/java/net/infumia/pack/GlyphImagePrepared.java new file mode 100644 index 0000000..0be44e0 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImagePrepared.java @@ -0,0 +1,48 @@ +package net.infumia.pack; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; + +final class GlyphImagePrepared implements GlyphAppendable, GlyphColorable { + + private final Key key; + private final char character; + private final int width; + private TextColor color; + + GlyphImagePrepared( + final Key key, + final char character, + final int width, + final TextColor color + ) { + this.key = key; + this.character = character; + this.width = width; + this.color = color; + } + + @Override + public Component toAdventure() { + return Component.text(this.character) + .font(this.key) + .color(this.color == null ? NamedTextColor.BLACK : this.color); + } + + @Override + public int width() { + return this.width; + } + + @Override + public TextColor color() { + return this.color; + } + + @Override + public void updateColor(final TextColor color) { + this.color = color; + } +} diff --git a/common/src/main/java/net/infumia/pack/GlyphImpl.java b/common/src/main/java/net/infumia/pack/GlyphImpl.java new file mode 100644 index 0000000..92437e1 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/GlyphImpl.java @@ -0,0 +1,36 @@ +package net.infumia.pack; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import team.unnamed.creative.font.Font; + +final class GlyphImpl implements GlyphAppendable { + + static final GlyphAppendable DEFAULT_SPACE = new GlyphImpl(Font.MINECRAFT_DEFAULT, " ", 4); + + private final Component component; + private final int length; + + GlyphImpl(final Component component, final int length) { + this.component = component; + this.length = length; + } + + GlyphImpl(final Key key, final String text, final int length) { + this(Component.text(text).font(key), length); + } + + GlyphImpl(final Key key, final char[] text, final int length) { + this(key, new String(text), length); + } + + @Override + public Component toAdventure() { + return this.component; + } + + @Override + public int width() { + return this.length; + } +} diff --git a/common/src/main/java/net/infumia/pack/Glyphs.java b/common/src/main/java/net/infumia/pack/Glyphs.java new file mode 100644 index 0000000..2c4b8c0 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Glyphs.java @@ -0,0 +1,29 @@ +package net.infumia.pack; + +/** + * Utility class for working with glyphs. + */ +public final class Glyphs { + + /** + * Returns an empty glyph instance. + * + * @return The empty glyph. + */ + public static Glyph empty() { + return GlyphEmpty.INSTANCE; + } + + /** + * Returns a space glyph instance. + * + * @return The space glyph. + */ + public static GlyphAppendable space() { + return GlyphImpl.DEFAULT_SPACE; + } + + private Glyphs() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/Internal.java b/common/src/main/java/net/infumia/pack/Internal.java new file mode 100644 index 0000000..b5f7a6e --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Internal.java @@ -0,0 +1,61 @@ +package net.infumia.pack; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.util.List; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.base.Writable; + +final class Internal { + + private static final String RESOURCES_FOLDER = "pack-resources"; + static final String DEFAULT_NAMESPACE = "pack"; + static final Key DEFAULT_SPACES_TEXTURE_KEY = Key.key(Internal.DEFAULT_NAMESPACE, "spaces"); + static final int SEPARATOR_WIDTH = 1; + + static Key keyWithPngExtension(final Key key) { + //noinspection PatternValidation + return Key.key(key.namespace(), key.value().concat(".png")); + } + + static Writable resourceFromJar(final String fileName) { + return Writable.resource( + Internal.class.getClassLoader(), + Internal.RESOURCES_FOLDER + "/" + fileName + ); + } + + static int calculateWidth( + final BufferedImage image, + final int fromX, + final int fromY, + final int toX, + final int toY + ) { + int width; + for (width = toX - 1; width > fromX; width--) { + for (int height = fromY; height < toY; height++) { + if (new Color(image.getRGB(width, height), true).getAlpha() != 0) { + return width - fromX + 1; + } + } + } + return width - fromX + 1; + } + + static int calculateWidth(final BufferedImage image) { + return Internal.calculateWidth(image, 0, 0, image.getWidth(), image.getHeight()); + } + + static char[] toCharArray(final List list) { + final char[] charArray = new char[list.size()]; + for (int i = 0; i < list.size(); i++) { + charArray[i] = list.get(i); + } + return charArray; + } + + private Internal() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/Kyori.java b/common/src/main/java/net/infumia/pack/Kyori.java new file mode 100644 index 0000000..5d65ca8 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Kyori.java @@ -0,0 +1,81 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.LinkedList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.text.flattener.FlattenerListener; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; + +final class Kyori { + + static Collection toColoredParts(final Component component) { + final Collection result = new ArrayList<>(); + ComponentFlattener.basic().flatten(component, new ColoredPartsFlattenerListener(result)); + return result; + } + + static final class ColoredComponentTextPart { + + private final String text; + private final TextColor color; + + private ColoredComponentTextPart(final String text, final TextColor color) { + this.text = text; + this.color = color; + } + + public String text() { + return this.text; + } + + public TextColor color() { + return this.color; + } + } + + private static final class ColoredPartsFlattenerListener implements FlattenerListener { + + private final Deque colors = new LinkedList<>(); + private final Collection result; + + private ColoredPartsFlattenerListener(final Collection result) { + this.result = result; + } + + @Override + public void pushStyle(final Style style) { + final TextColor color = style.color(); + if (color != null) { + this.colors.add(color); + } + } + + @Override + public void component(@NotNull final String text) { + this.result.add(new ColoredComponentTextPart(text, this.current())); + } + + @Override + public void popStyle(@NotNull final Style style) { + final TextColor color = style.color(); + if (color != null) { + this.colors.removeLast(); + } + } + + private TextColor current() { + final TextColor color = this.colors.peekLast(); + return color != null ? color : NamedTextColor.WHITE; + } + } + + private Kyori() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/Lazy.java b/common/src/main/java/net/infumia/pack/Lazy.java new file mode 100644 index 0000000..fad6e54 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Lazy.java @@ -0,0 +1,33 @@ +package net.infumia.pack; + +import java.util.function.Supplier; + +final class Lazy implements Supplier { + + static Lazy of(final Supplier supplier) { + return new Lazy<>(supplier); + } + + private final Supplier supplier; + + private volatile T value; + + private Lazy(final Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + T val = this.value; + if (val == null) { + synchronized (this) { + val = this.value; + if (val == null) { + val = this.supplier.get(); + this.value = val; + } + } + } + return val; + } +} diff --git a/common/src/main/java/net/infumia/pack/MultiMap.java b/common/src/main/java/net/infumia/pack/MultiMap.java new file mode 100644 index 0000000..0cfec3f --- /dev/null +++ b/common/src/main/java/net/infumia/pack/MultiMap.java @@ -0,0 +1,41 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +final class MultiMap { + + private final Map> map = new HashMap<>(); + + MultiMap() {} + + void put(final K key, final V value) { + this.map.computeIfAbsent(key, __ -> new ArrayList<>()).add(value); + } + + Collection get(final K key) { + return this.map.get(key); + } + + boolean remove(final K key, final V value) { + final Collection values = this.map.get(key); + if (values == null) { + return false; + } + final boolean removed = values.remove(value); + if (values.isEmpty()) { + this.map.remove(key); + } + return removed; + } + + void clear() { + this.map.clear(); + } + + Collection keys() { + return this.map.keySet(); + } +} diff --git a/common/src/main/java/net/infumia/pack/Pack.java b/common/src/main/java/net/infumia/pack/Pack.java new file mode 100644 index 0000000..bb15c2f --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Pack.java @@ -0,0 +1,92 @@ +package net.infumia.pack; + +import java.util.Collection; +import org.jetbrains.annotations.Contract; +import team.unnamed.creative.ResourcePack; + +/** + * An interface represents a collection of resources and provides methods to manage and manipulate them. + */ +public interface Pack { + /** + * Retrieves all file resources in this pack. + * + * @return An unmodifiable collection of {@link FileResource} objects. + */ + Collection all(); + + /** + * Compiles all resources in this pack, if applicable. + */ + void compileAll(); + + /** + * Adds a resource producer identified by the given ID to this pack. + * + * @param id The identifier for the resource producer. Cannot be null. + * @param producer The resource producer to add. Cannot be null. + * @param The type of the resource producer. Cannot be null. + * @return This pack instance. + */ + @Contract("_, _ -> this") + Pack with(ResourceIdentifier id, T producer); + + /** + * Adds a file resource to this pack. + * + * @param resource The file resource to add. Cannot be null. + * @return This pack instance. + */ + @Contract("_ -> this") + Pack with(FileResource resource); + + /** + * Adds Mojang-specific spaces resource to this pack. + *

+ * Available after pack format 9. + * + * @return This pack instance. + */ + @Contract("-> this") + default Pack withMojangSpaces() { + return this.with(ResourceIdentifiers.SPACES, ResourceProducers.spacesMojang()); + } + + /** + * Adds the default spaces resource producer to the pack. + * + * @return This Pack instance with the default spaces added. + */ + @Contract("-> this") + default Pack withBitmapSpaces() { + return this.with(ResourceIdentifiers.SPACES, ResourceProducers.spacesBitmap()); + } + + /** + * Retrieves the resource producer associated with the given identifier. + * + * @param id The identifier for the resource producer. Cannot be null. + * @param The type of the resource producer. + * @return The resource producer instance. + * @throws IllegalArgumentException If the identifier is not found in the pack. + */ + T get(ResourceIdentifier id) throws IllegalArgumentException; + + /** + * Convenience method to retrieve the Mojang-specific spaces resource producer. + * + * @return The Mojang-specific spaces resource producer. + */ + default ResourceProducerSpaces spaces() { + return this.get(ResourceIdentifiers.SPACES); + } + + /** + * Writes all file resources in this pack to the provided ResourcePack. + * + * @param resourcePack The target ResourcePack to write resources to. Cannot be null. + */ + default void writeAll(final ResourcePack resourcePack) { + this.all().forEach(resource -> resource.write(resourcePack)); + } +} diff --git a/common/src/main/java/net/infumia/pack/PackDefault.java b/common/src/main/java/net/infumia/pack/PackDefault.java new file mode 100644 index 0000000..e42b9a2 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/PackDefault.java @@ -0,0 +1,76 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class PackDefault implements Pack { + + private final FileResourceCompiler compiler; + private final FileResourceMerger merger; + private final Map raw = new HashMap<>(); + private final Map compiled = new HashMap<>(); + private final List resources = new ArrayList<>(); + + PackDefault(final FileResourceCompiler compiler, final FileResourceMerger merger) { + this.compiler = compiler; + this.merger = merger; + } + + @Override + public Collection all() { + if (!this.raw.isEmpty()) { + this.compileAll(); + } + return this.resources; + } + + @Override + public void compileAll() { + final Collection resources = this.compiler.compile(this.raw.values()); + this.resources.addAll(resources); + final Collection mergedResources = this.merger.merge(this.resources); + this.resources.clear(); + this.resources.addAll(mergedResources); + this.compiled.putAll(this.raw); + this.raw.clear(); + } + + @Override + public Pack with( + final ResourceIdentifier id, + final T producer + ) { + if (this.raw.containsKey(id.key()) || this.compiled.containsKey(id.key())) { + throw new IllegalArgumentException( + "Producer with " + id.key() + " identifier already registered" + ); + } + this.raw.put(id.key(), producer); + return this; + } + + @Override + public Pack with(final FileResource resource) { + this.resources.add(resource); + return this; + } + + @Override + public T get(final ResourceIdentifier id) + throws IllegalArgumentException { + if (!this.compiled.containsKey(id.key())) { + throw new IllegalArgumentException( + "Producer with " + id.key() + " identifier is not compiled" + ); + } + final ResourceProducer producer = this.compiled.get(id.key()); + if (!id.type().isAssignableFrom(producer.getClass())) { + throw new IllegalArgumentException("Wrong producer type"); + } + //noinspection unchecked + return (T) producer; + } +} diff --git a/common/src/main/java/net/infumia/pack/Packs.java b/common/src/main/java/net/infumia/pack/Packs.java new file mode 100644 index 0000000..9cb8e21 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/Packs.java @@ -0,0 +1,34 @@ +package net.infumia.pack; + +/** + * Utility class for creating instances of {@link Pack}. + */ +public final class Packs { + + /** + * Creates a new Pack instance with the specified file resource compiler. + * + * @param compiler The file resource compiler to use. Cannot be null. + * @param merger The merger to be used to merge same file resources. Cannot be null. + * @return A new Pack instance. + */ + public static Pack create( + final FileResourceCompiler compiler, + final FileResourceMerger merger + ) { + return new PackDefault(compiler, merger); + } + + /** + * Creates a new Pack instance with a default simple file resource compiler. + * + * @return A new Pack instance. + */ + public static Pack create() { + return Packs.create(FileResourceCompilers.simple(), FileResourceMergers.simple()); + } + + private Packs() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceIdentifier.java b/common/src/main/java/net/infumia/pack/ResourceIdentifier.java new file mode 100644 index 0000000..beec92b --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceIdentifier.java @@ -0,0 +1,22 @@ +package net.infumia.pack; + +/** + * Represents a resource identifier associated with a specific type of resource producer. + * + * @param The type of resource producer associated with this identifier. + */ +public interface ResourceIdentifier { + /** + * Returns the key associated with this resource identifier. + * + * @return The key as a string. + */ + String key(); + + /** + * Returns the class object representing the type of resource producer associated with this identifier. + * + * @return The class object of type T. + */ + Class type(); +} diff --git a/common/src/main/java/net/infumia/pack/ResourceIdentifierImage.java b/common/src/main/java/net/infumia/pack/ResourceIdentifierImage.java new file mode 100644 index 0000000..0c4916c --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceIdentifierImage.java @@ -0,0 +1,16 @@ +package net.infumia.pack; + +/** + * Represents a resource identifier specifically for {@link GlyphImage} instances. + */ +public interface ResourceIdentifierImage extends ResourceIdentifier { + /** + * Retrieves the type of resource this identifier represents, which is {@link GlyphImage}. + * + * @return The class object representing {@link GlyphImage}. + */ + @Override + default Class type() { + return GlyphImage.class; + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceIdentifierImpl.java b/common/src/main/java/net/infumia/pack/ResourceIdentifierImpl.java new file mode 100644 index 0000000..82fbed1 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceIdentifierImpl.java @@ -0,0 +1,22 @@ +package net.infumia.pack; + +final class ResourceIdentifierImpl implements ResourceIdentifier { + + private final String id; + private final Class type; + + ResourceIdentifierImpl(final String id, final Class type) { + this.id = id; + this.type = type; + } + + @Override + public Class type() { + return this.type; + } + + @Override + public String key() { + return this.id; + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceIdentifiers.java b/common/src/main/java/net/infumia/pack/ResourceIdentifiers.java new file mode 100644 index 0000000..cb28305 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceIdentifiers.java @@ -0,0 +1,17 @@ +package net.infumia.pack; + +/** + * Utility class containing resource identifiers for various resource types. + */ +public final class ResourceIdentifiers { + + /** + * Resource identifier for spaces resource producer. + */ + public static final ResourceIdentifier SPACES = + new ResourceIdentifierImpl<>("spaces", ResourceProducerSpaces.class); + + private ResourceIdentifiers() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducer.java b/common/src/main/java/net/infumia/pack/ResourceProducer.java new file mode 100644 index 0000000..2ae3be3 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducer.java @@ -0,0 +1,49 @@ +package net.infumia.pack; + +import java.util.Collection; +import java.util.Collections; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Keyed; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.part.ResourcePackPart; +import team.unnamed.creative.texture.Texture; + +/** + * Interface representing a producer of {@link ResourcePackPart}, identified by a unique key. + */ +public interface ResourceProducer extends Keyed { + /** + * Checks if the resource has been produced. + * + * @return {@code true} if the resource has been produced, {@code false} otherwise. + */ + boolean produced(); + + /** + * Produces the resource using the provided {@link ArbitraryCharacterFactory}. + * + * @param characterFactory the character factory used to produce the resource. Cannot be null. + * @throws ResourceAlreadyProducedException if the resource has already been produced. + */ + void produce(ArbitraryCharacterFactory characterFactory) + throws ResourceAlreadyProducedException; + + /** + * Retrieves the collection of {@link FontProvider} instances associated with the produced resource. + * + * @return an unmodifiable collection of font providers. + * @throws ResourceNotProducedException if the resource has not been produced. + */ + Collection fontProviders() throws ResourceNotProducedException; + + /** + * Retrieves the collection of {@link Texture} instances associated with the produced resource. + * + * @return a unmodifiable collection of textures. + * @throws ResourceNotProducedException if the resource has not been produced. + */ + default Collection textures() throws ResourceNotProducedException { + return Collections.emptyList(); + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducerSpaces.java b/common/src/main/java/net/infumia/pack/ResourceProducerSpaces.java new file mode 100644 index 0000000..00825c7 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducerSpaces.java @@ -0,0 +1,17 @@ +package net.infumia.pack; + +import net.infumia.pack.exception.ResourceNotProducedException; + +/** + * Represents a resource producer that generates space glyphs. + */ +public interface ResourceProducerSpaces extends ResourceProducer { + /** + * Translates the specified length into a space glyph. + * + * @param length The length of the space glyph to produce. + * @return The space glyph of the specified length. + * @throws ResourceNotProducedException if the space glyph cannot be produced. + */ + Glyph translate(int length) throws ResourceNotProducedException; +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducerSpacesAbstract.java b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesAbstract.java new file mode 100644 index 0000000..e08b70c --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesAbstract.java @@ -0,0 +1,55 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; + +abstract class ResourceProducerSpacesAbstract implements ResourceProducerSpaces { + + private final Key fontKey; + protected Map mapping; + + protected ResourceProducerSpacesAbstract(final Key fontKey) { + this.fontKey = fontKey; + } + + @NotNull + @Override + public final Key key() { + return this.fontKey; + } + + @Override + public boolean produced() { + return this.mapping != null; + } + + @Override + public Glyph translate(final int length) throws ResourceNotProducedException { + if (this.mapping == null) { + throw new ResourceNotProducedException(); + } + if (length == 0) { + return Glyphs.empty(); + } + final int sign = length > 0 ? 1 : -1; + final String binaryString = Integer.toBinaryString(Math.abs(length)); + final List characters = new ArrayList<>(); + int currentRankLength = 1; + for (int index = 0; index < binaryString.length(); index++) { + final char digit = binaryString.charAt(binaryString.length() - index - 1); + if (digit == '1') { + final int partLength = currentRankLength * sign; + if (!this.mapping.containsKey(partLength)) { + throw new IllegalArgumentException("Too much length"); + } + characters.add(this.mapping.get(partLength)); + } + currentRankLength *= 2; + } + return new GlyphImpl(this.fontKey, Internal.toCharArray(characters), length); + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducerSpacesBitmap.java b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesBitmap.java new file mode 100644 index 0000000..e98b172 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesBitmap.java @@ -0,0 +1,83 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.BitMapFontProvider; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +final class ResourceProducerSpacesBitmap extends ResourceProducerSpacesAbstract { + + private final Key textureKey; + private final Writable writable; + + private List textures; + private List fontProviders; + + ResourceProducerSpacesBitmap(final Key fontKey, final Key textureKey, final Writable writable) { + super(fontKey); + this.textureKey = Internal.keyWithPngExtension(textureKey); + this.writable = writable; + } + + @Override + public boolean produced() { + return this.textures != null; + } + + @Override + public void produce(final ArbitraryCharacterFactory characterFactory) { + if (this.textures != null) { + throw new ResourceAlreadyProducedException(); + } + this.mapping = new HashMap<>(); + final List fontProviders = new ArrayList<>(); + for (int length = 1; length <= 2048; length *= 2) { + fontProviders.add(this.prepareBuilder(characterFactory, length).build()); + fontProviders.add(this.prepareBuilder(characterFactory, length * (-1)).build()); + } + this.textures = Collections.singletonList(Texture.texture(this.textureKey, this.writable)); + this.fontProviders = fontProviders; + } + + @Override + public Collection fontProviders() throws ResourceNotProducedException { + if (this.fontProviders == null) { + throw new ResourceNotProducedException(); + } + return this.fontProviders; + } + + @Override + public Collection textures() throws ResourceNotProducedException { + if (this.textures == null) { + throw new ResourceNotProducedException(); + } + return this.textures; + } + + private BitMapFontProvider.Builder prepareBuilder( + final ArbitraryCharacterFactory characterFactory, + final int length + ) { + final BitMapFontProvider.Builder builder = FontProvider.bitMap(); + final char character = characterFactory.create(); + builder.characters(String.valueOf(character)); + builder.file(this.textureKey); + if (length > 0) { + builder.height(length - 1); + } else { + builder.height(length - 2); + builder.ascent(Short.MIN_VALUE); + } + this.mapping.put(length, character); + return builder; + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducerSpacesMojang.java b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesMojang.java new file mode 100644 index 0000000..b3f0b22 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducerSpacesMojang.java @@ -0,0 +1,58 @@ +package net.infumia.pack; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.font.SpaceFontProvider; + +final class ResourceProducerSpacesMojang extends ResourceProducerSpacesAbstract { + + private Collection fontProviders; + + ResourceProducerSpacesMojang(final Key fontKey) { + super(fontKey); + } + + @Override + public boolean produced() { + return this.fontProviders != null; + } + + @Override + public void produce(final ArbitraryCharacterFactory characterFactory) { + if (this.fontProviders != null) { + throw new ResourceAlreadyProducedException(); + } + this.mapping = new HashMap<>(); + final SpaceFontProvider.Builder fontProviderBuilder = FontProvider.space(); + for (int length = 1; length <= 2048; length *= 2) { + fontProviderBuilder.advance(this.retrieveCharacter(characterFactory, length), length); + fontProviderBuilder.advance( + this.retrieveCharacter(characterFactory, length * (-1)), + length * (-1) + ); + } + this.fontProviders = Collections.singletonList(fontProviderBuilder.build()); + } + + @Override + public Collection fontProviders() throws ResourceNotProducedException { + if (this.fontProviders == null) { + throw new ResourceNotProducedException(); + } + return this.fontProviders; + } + + private char retrieveCharacter( + final ArbitraryCharacterFactory characterFactory, + final int length + ) { + final char character = characterFactory.create(); + this.mapping.put(length, character); + return character; + } +} diff --git a/common/src/main/java/net/infumia/pack/ResourceProducers.java b/common/src/main/java/net/infumia/pack/ResourceProducers.java new file mode 100644 index 0000000..2b63aee --- /dev/null +++ b/common/src/main/java/net/infumia/pack/ResourceProducers.java @@ -0,0 +1,136 @@ +package net.infumia.pack; + +import net.kyori.adventure.key.Key; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.atlas.AtlasSource; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.model.ItemOverride; +import team.unnamed.creative.model.ItemPredicate; +import team.unnamed.creative.model.Model; +import team.unnamed.creative.model.ModelTexture; +import team.unnamed.creative.model.ModelTextures; +import team.unnamed.creative.texture.Texture; + +/** + * Utility class for creating instances of {@link ResourceProducer}. + */ +public final class ResourceProducers { + + /** + * Creates a space producer with specified font key, texture key, and writable. + * + * @param fontKey The font key. Cannot be null. + * @param textureKey The texture key. Cannot be null. + * @param spacesWritable The writable instance for spaces. Cannot be null. + * @return A {@link ResourceProducerSpaces} instance. + */ + public static ResourceProducerSpaces spacesBitmap( + final Key fontKey, + final Key textureKey, + final Writable spacesWritable + ) { + return new ResourceProducerSpacesBitmap(fontKey, textureKey, spacesWritable); + } + + /** + * Creates a {@link ResourceProducerSpaces} instance with default parameters. + * + * @return A {@link ResourceProducerSpaces} instance. + */ + public static ResourceProducerSpaces spacesBitmap() { + return ResourceProducers.spacesBitmap( + Font.MINECRAFT_DEFAULT, + Internal.DEFAULT_SPACES_TEXTURE_KEY, + Internal.resourceFromJar("space.png") + ); + } + + /** + * Creates a Mojang-specific space producer with the specified key. + * + * @param fontKey The font key for the Mojang space producer. Cannot be null. + * @return A {@link ResourceProducerSpaces} instance for Mojang. + */ + public static ResourceProducerSpaces spacesMojang(final Key fontKey) { + return new ResourceProducerSpacesMojang(fontKey); + } + + /** + * Creates a Mojang-specific space producer with default parameters. + * + * @return A {@link ResourceProducerSpaces} instance for Mojang. + */ + public static ResourceProducerSpaces spacesMojang() { + return ResourceProducers.spacesMojang(Font.MINECRAFT_DEFAULT); + } + + /** + * Creates a {@link GlyphImage} instance with the specified key, texture, and properties. + * + * @param fontKey The font key associated with the glyph image. Cannot be null. + * @param texture The texture of the glyph image. Cannot be null. + * @param properties The properties of the glyph image. Cannot be null. + * @return A {@link GlyphImage} instance. + */ + public static GlyphImage image( + final Key fontKey, + final Texture texture, + final TextureProperties properties + ) { + return new GlyphImageImpl(fontKey, texture, properties); + } + + /** + * Creates an item resource with the specified parameters. + * + * @param itemKey The key for the item. Cannot be null. + * @param overriddenItemKey The key for the base model. Cannot be null. + * @param itemImage The writable image for the blank slot. Cannot be null. + * @param customModelData The custom model data value. + * @return A {@link FileResource} representing the created item. + */ + public static FileResource item( + final Key itemKey, + final Key overriddenItemKey, + final Writable itemImage, + final int customModelData + ) { + return FileResources.all( + FileResources.model( + Model.model() + .key(itemKey) + .parent(Model.ITEM_GENERATED) + .textures(ModelTextures.builder().layers(ModelTexture.ofKey(itemKey)).build()) + .build() + ), + FileResources.model( + Model.model() + .key(overriddenItemKey) + .parent(Model.ITEM_GENERATED) + .textures( + ModelTextures.builder() + .layers(ModelTexture.ofKey(overriddenItemKey)) + .build() + ) + .overrides( + ItemOverride.of(itemKey, ItemPredicate.customModelData(customModelData)) + ) + .build() + ), + FileResources.texture( + Texture.texture(Internal.keyWithPngExtension(itemKey), itemImage) + ), + FileResources.atlas( + Atlas.atlas() + .key(Atlas.BLOCKS) + .sources(AtlasSource.directory(itemKey.namespace(), itemKey.namespace() + "/")) + .build() + ) + ); + } + + private ResourceProducers() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/common/src/main/java/net/infumia/pack/TextureProperties.java b/common/src/main/java/net/infumia/pack/TextureProperties.java new file mode 100644 index 0000000..c2e29b6 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/TextureProperties.java @@ -0,0 +1,65 @@ +package net.infumia.pack; + +import java.util.Objects; + +/** + * Represents the properties of a texture, including its height and ascent. + */ +public final class TextureProperties { + + private final int height; + private final int ascent; + + /** + * Ctor. + * + * @param height The height of the texture. + * @param ascent The ascent of the texture. + */ + public TextureProperties(final int height, final int ascent) { + this.height = height; + this.ascent = ascent; + } + + /** + * Retrieves the height of the texture. + * + * @return The height of the texture. + */ + public int height() { + return this.height; + } + + /** + * Retrieves the ascent of the texture. + * + * @return The ascent of the texture. + */ + public int ascent() { + return this.ascent; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final TextureProperties that = (TextureProperties) o; + return this.height == that.height && this.ascent == that.ascent; + } + + @Override + public int hashCode() { + return Objects.hash(this.height, this.ascent); + } + + @Override + public String toString() { + return ( + "TextureProperties[" + "height=" + this.height + ", " + "ascent=" + this.ascent + ']' + ); + } +} diff --git a/common/src/main/java/net/infumia/pack/exception/ResourceAlreadyProducedException.java b/common/src/main/java/net/infumia/pack/exception/ResourceAlreadyProducedException.java new file mode 100644 index 0000000..72290f4 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/exception/ResourceAlreadyProducedException.java @@ -0,0 +1,6 @@ +package net.infumia.pack.exception; + +/** + * Exception thrown when an attempt is made to produce a resource that has already been produced. + */ +public final class ResourceAlreadyProducedException extends RuntimeException {} diff --git a/common/src/main/java/net/infumia/pack/exception/ResourceNotProducedException.java b/common/src/main/java/net/infumia/pack/exception/ResourceNotProducedException.java new file mode 100644 index 0000000..9a4a518 --- /dev/null +++ b/common/src/main/java/net/infumia/pack/exception/ResourceNotProducedException.java @@ -0,0 +1,6 @@ +package net.infumia.pack.exception; + +/** + * Exception thrown when an attempt is made to access a resource that has not been produced. + */ +public final class ResourceNotProducedException extends RuntimeException {} diff --git a/common/src/main/resources/pack-resources/space.png b/common/src/main/resources/pack-resources/space.png new file mode 100644 index 0000000..d59960a Binary files /dev/null and b/common/src/main/resources/pack-resources/space.png differ diff --git a/generator/build.gradle.kts b/generator/build.gradle.kts new file mode 100644 index 0000000..acbe1ba --- /dev/null +++ b/generator/build.gradle.kts @@ -0,0 +1,12 @@ +import net.infumia.gradle.publish + +publish("generator") + +dependencies { + compileOnly(project(":common")) + compileOnly(project(":blank")) + compileOnly(project(":language")) + + compileOnly(libs.creative.serializer) + compileOnly(libs.jackson.databind) +} diff --git a/generator/src/main/java/net/infumia/pack/PackGeneratedContext.java b/generator/src/main/java/net/infumia/pack/PackGeneratedContext.java new file mode 100644 index 0000000..7643eee --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackGeneratedContext.java @@ -0,0 +1,82 @@ +package net.infumia.pack; + +import java.nio.file.Path; +import java.util.StringJoiner; +import team.unnamed.creative.ResourcePack; + +/** + * Context for generated resource pack. + */ +public final class PackGeneratedContext { + + private final ResourcePack resourcePack; + private final Pack pack; + private final Path outputDirectory; + private final Path outputFile; + + /** + * Ctor. + * + * @param resourcePack the resource pack. Cannot be null. + * @param pack the pack. Cannot be null. + * @param outputDirectory the output directory. Can be null. + * @param outputFile the output file. Can be null. + */ + PackGeneratedContext( + final ResourcePack resourcePack, + final Pack pack, + final Path outputDirectory, + final Path outputFile + ) { + this.resourcePack = resourcePack; + this.pack = pack; + this.outputDirectory = outputDirectory; + this.outputFile = outputFile; + } + + /** + * Returns the resource pack. + * + * @return the resource pack. + */ + public ResourcePack resourcePack() { + return this.resourcePack; + } + + /** + * Returns the pack. + * + * @return the pack. + */ + public Pack pack() { + return this.pack; + } + + /** + * Returns the output directory. + * + * @return the output directory. Can be null. + */ + public Path outputDirectory() { + return this.outputDirectory; + } + + /** + * Returns the output file. + * + * @return the output file. Can be null. + */ + public Path outputFile() { + return this.outputFile; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackGeneratedContext.class.getSimpleName() + "[", "]") + .add("resourcePack=" + this.resourcePack) + .add("pack=" + this.pack) + .add("outputDirectory=" + this.outputDirectory) + .add("outputFile=" + this.outputFile) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackGenerator.java b/generator/src/main/java/net/infumia/pack/PackGenerator.java new file mode 100644 index 0000000..441d521 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackGenerator.java @@ -0,0 +1,77 @@ +package net.infumia.pack; + +import java.io.IOException; + +/** + * Utility class for generating resource packs. + */ +public final class PackGenerator { + + /** + * Generates a resource pack based on the provided settings and base pack. + * + * @param readerSettings the pack reader settings. Cannot be null. + * @param writerSettings the pack writer settings. Cannot be null. + * @param base the base pack. Cannot be null. + * @return the generated pack context. + * @throws IOException if an I/ O error is thrown when accessing the starting file. + */ + public static PackGeneratedContext generate( + final PackReaderSettings readerSettings, + final PackWriterSettings writerSettings, + final Pack base + ) throws IOException { + return PackGenerator.write( + writerSettings, + PackParser.parse(PackGenerator.read(readerSettings, base)) + ); + } + + /** + * Generates a resource pack based on the provided settings with a default base pack. + * + * @param readerSettings the pack reader settings. Cannot be null. + * @param writerSettings the pack writer settings. Cannot be null. + * @return the generated pack context. + * @throws IOException if an I/ O error is thrown when accessing the starting file. + */ + public static PackGeneratedContext generate( + final PackReaderSettings readerSettings, + final PackWriterSettings writerSettings + ) throws IOException { + return PackGenerator.generate(readerSettings, writerSettings, Packs.create()); + } + + /** + * Reads the pack based on the provided settings and base pack. + * + * @param readerSettings the pack reader settings. Cannot be null. + * @param base the base pack. Cannot be null. + * @return the pack generation context. + * @throws IOException if an I/O error is thrown when accessing the starting file. + */ + public static PackGeneratorContext read( + final PackReaderSettings readerSettings, + final Pack base + ) throws IOException { + return new PackReader(readerSettings, base).read(); + } + + /** + * Writes the pack based on the provided settings and context. + * + * @param writerSettings the pack writer settings. Cannot be null. + * @param context the pack generator context. Cannot be null. + * @return the generated pack context. + */ + public static PackGeneratedContext write( + final PackWriterSettings writerSettings, + final PackGeneratorContext context + ) { + return new PackWriter(writerSettings).write(context); + } + + private PackGenerator() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackGeneratorContext.java b/generator/src/main/java/net/infumia/pack/PackGeneratorContext.java new file mode 100644 index 0000000..c29fb65 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackGeneratorContext.java @@ -0,0 +1,112 @@ +package net.infumia.pack; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.StringJoiner; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import team.unnamed.creative.ResourcePack; + +/** + * Context for resource pack generator. + */ +public final class PackGeneratorContext { + + private final ResourcePack resourcePack; + private final Pack pack; + private final PackReferenceMeta packReference; + private final Collection packPartReferences; + private final Path rootDirectory; + private final ComponentSerializer serializer; + + /** + * Ctor. + * + * @param resourcePack the resource pack. Cannot be null. + * @param pack the pack. Cannot be null. + * @param packReference the pack file reference. Cannot be null. + * @param packPartReferences the pack part references. Cannot be null. + * @param rootDirectory the root directory of the pack. Cannot be null. + * @param serializer the serializer to serialize components when needed. Cannot be null. + */ + PackGeneratorContext( + final ResourcePack resourcePack, + final Pack pack, + final PackReferenceMeta packReference, + final Collection packPartReferences, + final Path rootDirectory, + final ComponentSerializer serializer + ) { + this.resourcePack = resourcePack; + this.pack = pack; + this.packReference = packReference; + this.packPartReferences = Collections.unmodifiableCollection(packPartReferences); + this.rootDirectory = rootDirectory; + this.serializer = serializer; + } + + /** + * Returns the resource pack. + * + * @return the resource pack. + */ + public ResourcePack resourcePack() { + return this.resourcePack; + } + + /** + * Returns the pack. + * + * @return the pack. + */ + public Pack pack() { + return this.pack; + } + + /** + * Returns the pack reference. + * + * @return the pack reference. + */ + public PackReferenceMeta packReference() { + return this.packReference; + } + + /** + * Returns the pack part references. + * + * @return the pack part references. + */ + public Collection packPartReferences() { + return this.packPartReferences; + } + + /** + * Returns the root directory. + * + * @return the root directory. Can be null. + */ + public Path rootDirectory() { + return this.rootDirectory; + } + + /** + * Returns the component serializer. + * + * @return the component serializer. + */ + public ComponentSerializer serializer() { + return this.serializer; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackGeneratorContext.class.getSimpleName() + "[", "]") + .add("resourcePack=" + this.resourcePack) + .add("pack=" + this.pack) + .add("packReference=" + this.packReference) + .add("packPartReferences=" + this.packPartReferences) + .add("serializer=" + this.serializer) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackParser.java b/generator/src/main/java/net/infumia/pack/PackParser.java new file mode 100644 index 0000000..e3e89ea --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackParser.java @@ -0,0 +1,51 @@ +package net.infumia.pack; + +import java.util.Collection; +import team.unnamed.creative.metadata.pack.PackMeta; + +/** + * Utility class for parsing packs. + */ +public final class PackParser { + + /** + * Parses the given pack generator context. + * + * @param context the pack generator context. Cannot be null. + * @return the updated pack generator context. + */ + public static PackGeneratorContext parse(final PackGeneratorContext context) { + PackParser.parseMeta(context); + PackParser.parseParts(context); + context.pack().writeAll(context.resourcePack()); + return context; + } + + private static void parseMeta(final PackGeneratorContext context) { + final Pack pack = context.pack(); + final PackReferenceMeta meta = context.packReference(); + final PackMeta packMeta = meta.parsePackMeta(context.serializer()); + context.resourcePack().packMeta(packMeta); + if (meta.addBlankSlot()) { + pack.with(BlankSlot.get()); + } + if (meta.addSpaces()) { + if (packMeta.formats().min() >= 9) { + pack.withMojangSpaces(); + } else { + pack.withBitmapSpaces(); + } + } + } + + private static void parseParts(final PackGeneratorContext context) { + final Collection parts = context.packPartReferences(); + for (final PackReferencePart part : parts) { + part.add(context); + } + } + + private PackParser() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReadFilters.java b/generator/src/main/java/net/infumia/pack/PackReadFilters.java new file mode 100644 index 0000000..d67edb7 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReadFilters.java @@ -0,0 +1,24 @@ +package net.infumia.pack; + +import java.nio.file.Path; +import java.util.function.Predicate; + +/** + * Utility class for creating read filters for pack reading. + */ +public final class PackReadFilters { + + /** + * Creates a predicate that filters paths by the specified file extension. + * + * @param extension the file extension to filter by. Cannot be null. + * @return a predicate that returns true for paths with the specified extension. + */ + public static Predicate withExtension(final String extension) { + return path -> path.getFileName().toString().endsWith(extension); + } + + private PackReadFilters() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReader.java b/generator/src/main/java/net/infumia/pack/PackReader.java new file mode 100644 index 0000000..a8da00e --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReader.java @@ -0,0 +1,106 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import team.unnamed.creative.ResourcePack; + +final class PackReader { + + private final PackReaderSettings settings; + private final Pack base; + + private File packReferenceFile; + private ObjectReader packReader; + private ObjectReader packPartReader; + + PackReader(final PackReaderSettings settings, final Pack base) { + this.settings = settings; + this.base = base; + } + + PackGeneratorContext read() throws IOException { + this.prepare(); + + FileVisitOption[] visitOptions = this.settings.visitOptions(); + if (visitOptions == null) { + visitOptions = new FileVisitOption[0]; + } + try (final Stream walking = Files.walk(this.settings.root(), visitOptions)) { + return this.read0(walking); + } + } + + private PackGeneratorContext read0(@NotNull final Stream walking) throws IOException { + final PackReferenceMeta packReference = this.packReader.readValue(this.packReferenceFile); + final Collection packPartReferences = walking + .filter(Files::isRegularFile) + .filter(this.settings.readFilter()) + .map(Path::toFile) + .filter(file -> !this.packReferenceFile.equals(file)) + .map(file -> { + try ( + final MappingIterator iterator = + this.packPartReader.readValues(file) + ) { + final Path path = file.getParentFile().toPath(); + if (path.equals(this.settings.root())) { + return iterator.readAll(); + } else { + return iterator + .readAll() + .stream() + .map(part -> part.directory(path)) + .collect(Collectors.toList()); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + }) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return new PackGeneratorContext( + ResourcePack.resourcePack(), + this.base, + packReference, + packPartReferences, + this.settings.root(), + this.settings.serializer() + ); + } + + private void prepare() { + final Path root = this.settings.root(); + final Path packReferenceFile = root.resolve(this.settings.packReferenceFileName()); + if (Files.notExists(packReferenceFile)) { + throw new IllegalStateException( + "Pack reference file does not exist: " + packReferenceFile + ); + } + this.packReferenceFile = packReferenceFile.toFile(); + + final ObjectMapper mapper = this.settings.mapper(); + this.packReader = mapper.readerFor(Internal.PACK_TYPE); + this.packPartReader = mapper.readerFor(Internal.PACK_PART_TYPE); + } + + private static final class Internal { + + private static final TypeReference PACK_TYPE = new TypeReference< + PackReferenceMeta + >() {}; + private static final TypeReference PACK_PART_TYPE = new TypeReference< + PackReferencePart + >() {}; + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReaderSettings.java b/generator/src/main/java/net/infumia/pack/PackReaderSettings.java new file mode 100644 index 0000000..c5e0e8b --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReaderSettings.java @@ -0,0 +1,133 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.StringJoiner; +import java.util.function.Predicate; +import net.kyori.adventure.text.serializer.ComponentSerializer; + +/** + * Settings for reading a pack. + */ +public final class PackReaderSettings { + + private final Path root; + private final FileVisitOption[] visitOptions; + private final String packReferenceFileName; + private final ObjectMapper mapper; + private final Predicate readFilter; + private final ComponentSerializer serializer; + + /** + * Ctor. + * + * @param root the root path. + * @param visitOptions the visit options. Can be null. + * @param packReferenceFileName the pack reference file name. Cannot be null. + * @param mapper the object mapper to read pack and pack part reference files. Cannot be null. + * @param readFilter the read filter for {@link Files#walk(Path, FileVisitOption...)}. + * @param serializer the serializer to serialize components when needed. Cannot be null. + */ + public PackReaderSettings( + final Path root, + final FileVisitOption[] visitOptions, + final String packReferenceFileName, + final ObjectMapper mapper, + final Predicate readFilter, + final ComponentSerializer serializer + ) { + this.root = root; + this.visitOptions = visitOptions; + this.packReferenceFileName = packReferenceFileName; + this.mapper = mapper; + this.readFilter = readFilter; + this.serializer = serializer; + } + + /** + * Ctor. + * + * @param root the root path. + * @param packReferenceFileName the pack reference file name. Cannot be null. + * @param mapper the object mapper to read pack and pack part reference files. Cannot be null. + * @param readFilter the read filter for {@link Files#walk(Path, FileVisitOption...)}. + */ + public PackReaderSettings( + final Path root, + final String packReferenceFileName, + final ObjectMapper mapper, + final Predicate readFilter, + final ComponentSerializer serializer + ) { + this(root, null, packReferenceFileName, mapper, readFilter, serializer); + } + + /** + * Returns the root path. + * + * @return the root path. + */ + public Path root() { + return this.root; + } + + /** + * Returns the visit options. + * + * @return the visit options. + */ + public FileVisitOption[] visitOptions() { + return this.visitOptions; + } + + /** + * Returns the pack reference file name. + * + * @return the pack reference file name. + */ + public String packReferenceFileName() { + return this.packReferenceFileName; + } + + /** + * Returns the object mapper. + * + * @return the object mapper. + */ + public ObjectMapper mapper() { + return this.mapper; + } + + /** + * Returns the read filter. + * + * @return the read filter. + */ + public Predicate readFilter() { + return this.readFilter; + } + + /** + * Returns the component serializer. + * + * @return the component serializer. + */ + public ComponentSerializer serializer() { + return this.serializer; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackReaderSettings.class.getSimpleName() + "[", "]") + .add("root=" + this.root) + .add("visitOptions=" + Arrays.toString(this.visitOptions)) + .add("packReferenceFileName='" + this.packReferenceFileName + "'") + .add("mapper=" + this.mapper) + .add("readFilter=" + this.readFilter) + .add("serializer=" + this.serializer) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReferenceMeta.java b/generator/src/main/java/net/infumia/pack/PackReferenceMeta.java new file mode 100644 index 0000000..098d892 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReferenceMeta.java @@ -0,0 +1,129 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.StringJoiner; +import net.kyori.adventure.text.serializer.ComponentSerializer; +import team.unnamed.creative.metadata.pack.PackFormat; +import team.unnamed.creative.metadata.pack.PackMeta; + +/** + * Represents a reference to a pack with format constraints and a description. + */ +public final class PackReferenceMeta { + + private final Integer format; + private final Integer minimumFormat; + private final Integer maximumFormat; + private final String description; + private final boolean addBlankSlot; + private final boolean addSpaces; + private final String defaultNamespace; + + /** + * Ctor. + * + * @param format the pack format. Can be null + * @param minimumFormat the minimum pack format. Can be null + * @param maximumFormat the maximum pack format. Can be null + * @param description the description of the pack + * @param addBlankSlot adds the {@link BlankSlot} resources. + * @param addSpaces adds the {@link ResourceProducers#spacesBitmap()} or {@link ResourceProducers#spacesMojang()} based on the pack format. + * @param defaultNamespace the default namespace that will be used when a {@link PackReferencePart} does not have a namespace. Can be null. + */ + @JsonCreator + public PackReferenceMeta( + @JsonProperty("format") final Integer format, + @JsonProperty("minimum-format") final Integer minimumFormat, + @JsonProperty("maximum-format") final Integer maximumFormat, + @JsonProperty("description") final String description, + @JsonProperty("add-blank-slot") final boolean addBlankSlot, + @JsonProperty("add-spaces") final boolean addSpaces, + @JsonProperty("default-namespace") final String defaultNamespace + ) { + this.format = format; + this.minimumFormat = minimumFormat; + this.maximumFormat = maximumFormat; + this.description = description; + this.addBlankSlot = addBlankSlot; + this.addSpaces = addSpaces; + this.defaultNamespace = defaultNamespace; + } + + /** + * Returns whether the {@link BlankSlot} resources should be added. + * + * @return {@code true} if the {@link BlankSlot} resources should be added, {@code false} otherwise + */ + public boolean addBlankSlot() { + return this.addBlankSlot; + } + + /** + * Returns whether spaces should be added. + * + * @return {@code true} if spaces should be added, {@code false} otherwise + */ + public boolean addSpaces() { + return this.addSpaces; + } + + /** + * Returns the default namespace. + * + * @return the default namespace. Can be null. + */ + public String defaultNamespace() { + return this.defaultNamespace; + } + + /** + * Parses the pack formats to a {@link PackMeta} object. + * + * @param serializer the component serializer. Cannot be null. + * @return the generated {@link PackMeta}. + * @throws IllegalStateException if none of the {@link #format},{@link #minimumFormat}, and {@link #maximumFormat} are provided. + */ + public PackMeta parsePackMeta(final ComponentSerializer serializer) { + if (this.format == null && this.minimumFormat == null && this.maximumFormat == null) { + throw new IllegalStateException( + "At least one of format, minimumFormat and maximumFormat must be provided!" + ); + } + + final int minimumFormat; + if (this.format != null) { + minimumFormat = this.format; + } else if (this.minimumFormat != null) { + minimumFormat = this.minimumFormat; + } else { + minimumFormat = this.maximumFormat; + } + + final int maximumFormat; + if (this.format != null) { + maximumFormat = this.format; + } else if (this.maximumFormat != null) { + maximumFormat = this.maximumFormat; + } else { + maximumFormat = minimumFormat; + } + + return PackMeta.of( + PackFormat.format(minimumFormat, minimumFormat, maximumFormat), + serializer.deserialize(this.description) + ); + } + + @Override + public String toString() { + return new StringJoiner(", ", PackReferenceMeta.class.getSimpleName() + "[", "]") + .add("format=" + this.format) + .add("minimumFormat=" + this.minimumFormat) + .add("maximumFormat=" + this.maximumFormat) + .add("description='" + this.description + "'") + .add("addBlankSlot=" + this.addBlankSlot) + .add("addSpaces=" + this.addSpaces) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReferencePart.java b/generator/src/main/java/net/infumia/pack/PackReferencePart.java new file mode 100644 index 0000000..61fe1ef --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReferencePart.java @@ -0,0 +1,33 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.nio.file.Path; + +/** + * Abstract base class for a pack part reference. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes( + { + @JsonSubTypes.Type(value = PackReferencePartImage.class, name = "image"), + @JsonSubTypes.Type(value = PackReferencePartItem.class, name = "item"), + } +) +public abstract class PackReferencePart { + + /** + * Adds this part to the given pack generation context. + * + * @param context the pack generation context + */ + public abstract void add(PackGeneratorContext context); + + /** + * Sets the directory for this pack part reference. + * + * @param directory the directory path. Cannot be null. + * @return the updated pack part reference. + */ + abstract PackReferencePart directory(Path directory); +} diff --git a/generator/src/main/java/net/infumia/pack/PackReferencePartImage.java b/generator/src/main/java/net/infumia/pack/PackReferencePartImage.java new file mode 100644 index 0000000..9c58b28 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReferencePartImage.java @@ -0,0 +1,91 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.nio.file.Path; +import java.util.Locale; +import java.util.StringJoiner; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.texture.Texture; + +/** + * Represents an image part of a pack reference. + */ +public final class PackReferencePartImage extends PackReferencePart { + + @JsonProperty + private String namespace; + + @JsonProperty(required = true) + private String key; + + @JsonProperty(required = true) + private String image; + + @JsonProperty(required = true) + private int height; + + @JsonProperty(required = true) + private int ascent; + + private Path directory; + + @Override + public void add(final PackGeneratorContext context) { + final String namespace = this.namespace == null + ? context.packReference().defaultNamespace() + : this.namespace; + if (namespace == null) { + throw new IllegalStateException("Pack reference namespace cannot be null!"); + } + + final Path root = context.rootDirectory(); + + final String parent; + if (this.directory == null) { + parent = ""; + } else { + parent = root + .relativize(this.directory) + .toString() + .toLowerCase(Locale.ROOT) + .replace("\\", "/") + .replace(" ", "_") + + "/"; + } + + final String key = parent + this.key; + context + .pack() + .with( + (ResourceIdentifierImage) () -> key, + ResourceProducers.image( + Font.MINECRAFT_DEFAULT, + Texture.texture( + Key.key(namespace, key + ".png"), + Writable.path(root.resolve(parent + this.image)) + ), + new TextureProperties(this.height, this.ascent) + ) + ); + } + + @Override + PackReferencePartImage directory(final Path directory) { + this.directory = directory; + return this; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackReferencePartImage.class.getSimpleName() + "[", "]") + .add("namespace='" + this.namespace + "'") + .add("key='" + this.key + "'") + .add("image='" + this.image + "'") + .add("height=" + this.height) + .add("ascent=" + this.ascent) + .add("directory=" + this.directory) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackReferencePartItem.java b/generator/src/main/java/net/infumia/pack/PackReferencePartItem.java new file mode 100644 index 0000000..27a64cf --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackReferencePartItem.java @@ -0,0 +1,96 @@ +package net.infumia.pack; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.nio.file.Path; +import java.util.Locale; +import java.util.StringJoiner; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.base.Writable; + +/** + * Represents an item part of a pack reference. + */ +public final class PackReferencePartItem extends PackReferencePart { + + @JsonProperty + private String namespace; + + @JsonProperty(required = true) + private String key; + + @JsonProperty(value = "custom-model-data", required = true) + private int customModelData; + + @JsonProperty(required = true) + private String image; + + @JsonProperty("overridden-namespace") + private String overriddenNamespace; + + @JsonProperty(value = "overridden-key", required = true) + private String overriddenKey; + + private Path directory; + + @Override + public void add(final PackGeneratorContext context) { + final String namespace = this.namespace == null + ? context.packReference().defaultNamespace() + : this.namespace; + if (namespace == null) { + throw new IllegalStateException("Pack reference namespace cannot be null!"); + } + + final Path root = context.rootDirectory(); + + final String parent; + if (this.directory == null) { + parent = ""; + } else { + parent = root + .relativize(this.directory) + .toString() + .toLowerCase(Locale.ROOT) + .replace("\\", "/") + .replace(" ", "_") + + "/"; + } + + final Key overriddenItemKey; + if (this.overriddenNamespace == null) { + overriddenItemKey = Key.key(this.overriddenKey); + } else { + overriddenItemKey = Key.key(this.overriddenNamespace, this.overriddenKey); + } + + context + .pack() + .with( + ResourceProducers.item( + Key.key(namespace, parent + this.key), + overriddenItemKey, + Writable.path(root.resolve(parent + this.image)), + this.customModelData + ) + ); + } + + @Override + PackReferencePartItem directory(final Path directory) { + this.directory = directory; + return this; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackReferencePartItem.class.getSimpleName() + "[", "]") + .add("namespace='" + this.namespace + "'") + .add("key='" + this.key + "'") + .add("customModelData=" + this.customModelData) + .add("image='" + this.image + "'") + .add("overriddenNamespace='" + this.overriddenNamespace + "'") + .add("overriddenKey='" + this.overriddenKey + "'") + .add("directory=" + this.directory) + .toString(); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackWriter.java b/generator/src/main/java/net/infumia/pack/PackWriter.java new file mode 100644 index 0000000..9b39eba --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackWriter.java @@ -0,0 +1,28 @@ +package net.infumia.pack; + +import java.nio.file.Path; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter; + +final class PackWriter { + + private final PackWriterSettings settings; + + PackWriter(final PackWriterSettings settings) { + this.settings = settings; + } + + PackGeneratedContext write(final PackGeneratorContext context) { + final MinecraftResourcePackWriter writer = this.settings.writer(); + final Path outputDirectory = this.settings.outputDirectory(); + final ResourcePack resourcePack = context.resourcePack(); + if (outputDirectory != null) { + writer.writeToDirectory(outputDirectory.toFile(), resourcePack); + } + final Path outputFile = this.settings.outputFile(); + if (outputFile != null) { + writer.writeToZipFile(outputFile, resourcePack); + } + return new PackGeneratedContext(resourcePack, context.pack(), outputDirectory, outputFile); + } +} diff --git a/generator/src/main/java/net/infumia/pack/PackWriterSettings.java b/generator/src/main/java/net/infumia/pack/PackWriterSettings.java new file mode 100644 index 0000000..9b489a6 --- /dev/null +++ b/generator/src/main/java/net/infumia/pack/PackWriterSettings.java @@ -0,0 +1,66 @@ +package net.infumia.pack; + +import java.nio.file.Path; +import java.util.StringJoiner; +import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter; + +/** + * Settings for writing a resource pack. + */ +public final class PackWriterSettings { + + private final MinecraftResourcePackWriter writer; + private final Path outputDirectory; + private final Path outputFile; + + /** + * Ctor. + * + * @param writer the resource pack writer. Cannot be null. + * @param outputDirectory the output directory. Can be null. + * @param outputFile the output file. Can be null. + */ + public PackWriterSettings( + final MinecraftResourcePackWriter writer, + final Path outputDirectory, + final Path outputFile + ) { + this.writer = writer; + this.outputDirectory = outputDirectory; + this.outputFile = outputFile; + } + + /** + * Returns the resource pack writer. + * + * @return the resource pack writer. + */ + public MinecraftResourcePackWriter writer() { + return this.writer; + } + + /** + * Returns the output directory. + * + * @return the output directory. Can be null. + */ + public Path outputDirectory() { + return this.outputDirectory; + } + + /** + * Returns the output file. + * + * @return the output file. Can be null. + */ + public Path outputFile() { + return this.outputFile; + } + + @Override + public String toString() { + return new StringJoiner(", ", PackWriterSettings.class.getSimpleName() + "[", "]") + .add("writer=" + this.writer) + .toString(); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ddccbfa..f131868 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,11 @@ [versions] +creative = "1.7.2" [libraries] +adventure-api = { module = "net.kyori:adventure-api", version = "4.17.0" } +creative-api = { module = "team.unnamed:creative-api", version.ref = "creative" } +creative-serializer = { module = "team.unnamed:creative-serializer-minecraft", version.ref = "creative" } -[plugins] -nexus = { id = "com.vanniktech.maven.publish", version = "0.29.0" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version = "2.17.1" } +nexus-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.29.0" } +spotless-plugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.25.0" } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/language/build.gradle.kts b/language/build.gradle.kts new file mode 100644 index 0000000..0caefcb --- /dev/null +++ b/language/build.gradle.kts @@ -0,0 +1,9 @@ +import net.infumia.gradle.publish + +publish("language") + +dependencies { + compileOnly(project(":common")) + + compileOnly(libs.creative.api) +} diff --git a/language/src/main/java/net/infumia/pack/Language.java b/language/src/main/java/net/infumia/pack/Language.java new file mode 100644 index 0000000..11978cf --- /dev/null +++ b/language/src/main/java/net/infumia/pack/Language.java @@ -0,0 +1,61 @@ +package net.infumia.pack; + +import java.util.List; +import net.kyori.adventure.key.Key; +import team.unnamed.creative.texture.Texture; + +/** + * Utility class for creating resource producers related to languages and multi-character glyphs. + */ +public final class Language { + + /** + * Creates a {@link ResourceProducerLanguage} instance with the specified parameters. + * + * @param fontKey The key associated with the font. Cannot be null. + * @param texture The texture of the glyph collection. Cannot be null. + * @param propertiesList The list of texture properties for the glyphs. Cannot be null. + * @param charactersMapping The list of character mappings for the glyphs. Cannot be null. + * @return A {@link ResourceProducerLanguage} instance. + */ + public static ResourceProducerLanguage language( + final Key fontKey, + final Texture texture, + final List propertiesList, + final List charactersMapping + ) { + return new ResourceProducerLanguageImpl( + fontKey, + texture, + propertiesList, + charactersMapping + ); + } + + /** + * Creates a {@link ResourceProducerImageMultichar} instance with the specified parameters. + * + * @param fontKey The font key associated with the font. Cannot be null. + * @param texture The texture of the glyph collection. Cannot be null. + * @param properties The properties of the texture. Cannot be null. + * @param charactersMapping The list of character mappings for the glyphs. Cannot be null. + * @return A {@link ResourceProducerImageMultichar} instance. + */ + static ResourceProducerImageMultichar multichar( + final Key fontKey, + final Texture texture, + final TextureProperties properties, + final List charactersMapping + ) { + return new ResourceProducerImageMulticharImpl( + fontKey, + texture, + properties, + charactersMapping + ); + } + + private Language() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/language/src/main/java/net/infumia/pack/ResourceProducerImageMultichar.java b/language/src/main/java/net/infumia/pack/ResourceProducerImageMultichar.java new file mode 100644 index 0000000..850cfec --- /dev/null +++ b/language/src/main/java/net/infumia/pack/ResourceProducerImageMultichar.java @@ -0,0 +1,63 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.text.format.TextColor; + +/** + * Represents an interface for a glyph collection that supports multi-character glyphs. + */ +public interface ResourceProducerImageMultichar extends ResourceProducer { + /** + * Translates a character to a GlyphAppendable with the specified color. + * + * @param character The character to translate. + * @param color The color of the glyph. + * @return A GlyphAppendable representing the translated character. + * @throws IllegalArgumentException If translation fails. + */ + GlyphAppendable translate(char character, TextColor color) throws IllegalArgumentException; + + /** + * Translates a string of text to a list of GlyphAppendable with the specified color. + * + * @param text The text to translate. + * @param color The color of the glyphs. + * @return A list of GlyphAppendable representing the translated text. + * @throws IllegalArgumentException If translation fails. + */ + default List translate(final String text, final TextColor color) + throws IllegalArgumentException { + final List glyphs = new ArrayList<>(); + for (final char character : text.toCharArray()) { + if (character == ' ') { + glyphs.add(Glyphs.space()); + } else { + glyphs.add(this.translate(character, color)); + } + } + return glyphs; + } + + /** + * Translates a character to a GlyphAppendable without specifying a color. + * + * @param character The character to translate. + * @return A GlyphAppendable representing the translated character. + * @throws IllegalArgumentException If translation fails. + */ + default GlyphAppendable translate(final char character) throws IllegalArgumentException { + return this.translate(character, null); + } + + /** + * Translates a string of text to a list of GlyphAppendable without specifying a color. + * + * @param text The text to translate. + * @return A list of GlyphAppendable representing the translated text. + * @throws IllegalArgumentException If translation fails. + */ + default List translate(final String text) throws IllegalArgumentException { + return this.translate(text, null); + } +} diff --git a/language/src/main/java/net/infumia/pack/ResourceProducerImageMulticharImpl.java b/language/src/main/java/net/infumia/pack/ResourceProducerImageMulticharImpl.java new file mode 100644 index 0000000..6d6963d --- /dev/null +++ b/language/src/main/java/net/infumia/pack/ResourceProducerImageMulticharImpl.java @@ -0,0 +1,144 @@ +package net.infumia.pack; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.imageio.ImageIO; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import team.unnamed.creative.font.BitMapFontProvider; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +final class ResourceProducerImageMulticharImpl implements ResourceProducerImageMultichar { + + private final Map originToChar = new HashMap<>(); + + private final Key key; + private final Texture texture; + private final TextureProperties properties; + private final List charactersMapping; + + private List fontProviders; + private BufferedImage image; + + ResourceProducerImageMulticharImpl( + final Key key, + final Texture texture, + final TextureProperties properties, + final List charactersMapping + ) { + this.key = key; + this.texture = texture; + this.properties = properties; + this.charactersMapping = charactersMapping; + } + + @NotNull + @Override + public Key key() { + return this.key; + } + + @Override + public boolean produced() { + return this.fontProviders != null; + } + + @Override + public void produce(final ArbitraryCharacterFactory characterFactory) + throws ResourceAlreadyProducedException { + if (this.fontProviders != null) { + throw new ResourceAlreadyProducedException(); + } + final BitMapFontProvider.Builder fontProviderBuilder = FontProvider.bitMap(); + fontProviderBuilder.file(this.texture.key()); + fontProviderBuilder.ascent(this.properties.ascent()); + fontProviderBuilder.height(this.properties.height()); + final List mappingLines = new ArrayList<>(); + for (final String mappingLine : this.charactersMapping) { + final StringBuilder builder = new StringBuilder(); + for (final char character : mappingLine.toCharArray()) { + final char arbitraryCharacter = characterFactory.create(); + this.originToChar.put(character, arbitraryCharacter); + builder.append(arbitraryCharacter); + } + mappingLines.add(builder.toString()); + } + fontProviderBuilder.characters(mappingLines); + this.fontProviders = Collections.singletonList(fontProviderBuilder.build()); + } + + @Override + public Collection fontProviders() throws ResourceNotProducedException { + return this.fontProviders; + } + + @Override + public Collection textures() throws ResourceNotProducedException { + return Collections.singletonList(this.texture); + } + + @Override + public GlyphImagePrepared translate(final char character, final TextColor color) + throws IllegalArgumentException { + if (!this.originToChar.containsKey(character)) { + throw new IllegalArgumentException("Illegal character: " + character); + } + int width = 0; + for (int lineIndex = 0; lineIndex < this.charactersMapping.size(); lineIndex++) { + final String line = this.charactersMapping.get(lineIndex); + for ( + int characterIndex = 0; + characterIndex < line.toCharArray().length; + characterIndex++ + ) { + if (line.charAt(characterIndex) == character) { + if (this.image == null) { + this.cacheImage(); + } + if (this.image == null) { + throw new IllegalArgumentException( + "Image " + this.texture.key() + " not found" + ); + } + final int filePartWidth = + this.image.getWidth() / this.charactersMapping.get(0).length(); + final int filePartHeight = + this.image.getHeight() / this.charactersMapping.size(); + width = (int) Math.ceil( + ((double) this.properties.height() / (double) filePartHeight) * + Internal.calculateWidth( + this.image, + filePartWidth * characterIndex, + filePartHeight * lineIndex, + filePartWidth * (characterIndex + 1), + filePartHeight * (lineIndex + 1) + ) + ) + + Internal.SEPARATOR_WIDTH; + break; + } + } + } + + return new GlyphImagePrepared(this.key, this.originToChar.get(character), width, color); + } + + private void cacheImage() { + try { + this.image = ImageIO.read(new ByteArrayInputStream(this.texture.data().toByteArray())); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/language/src/main/java/net/infumia/pack/ResourceProducerLanguage.java b/language/src/main/java/net/infumia/pack/ResourceProducerLanguage.java new file mode 100644 index 0000000..68145ff --- /dev/null +++ b/language/src/main/java/net/infumia/pack/ResourceProducerLanguage.java @@ -0,0 +1,76 @@ +package net.infumia.pack; + +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; + +/** + * Represents an interface for a glyph collection that supports language-specific glyphs. + */ +public interface ResourceProducerLanguage extends ResourceProducer { + /** + * Translates a character to a GlyphAppendable with specified height, ascent, and color. + * + * @param height The height of the glyph. + * @param ascent The ascent of the glyph. + * @param character The character to translate. + * @param color The color of the glyph. + * @return A GlyphAppendable representing the translated character. + * @throws IllegalArgumentException If translation fails. + */ + GlyphAppendable translate(int height, int ascent, char character, TextColor color) + throws IllegalArgumentException; + + /** + * Translates a string of text to a list of GlyphAppendable with specified height, ascent, and color. + * + * @param height The height of the glyphs. + * @param ascent The ascent of the glyphs. + * @param text The text to translate. + * @param color The color of the glyphs. + * @return A list of GlyphAppendable representing the translated text. + * @throws IllegalArgumentException If translation fails. + */ + List translate(int height, int ascent, String text, TextColor color) + throws IllegalArgumentException; + + /** + * Translates a Component to a list of GlyphAppendable with specified height and ascent. + * + * @param height The height of the glyphs. + * @param ascent The ascent of the glyphs. + * @param component The component to translate. + * @return A list of GlyphAppendable representing the translated component. + * @throws IllegalArgumentException If translation fails. + */ + List translate(int height, int ascent, Component component) + throws IllegalArgumentException; + + /** + * Translates a character to a GlyphAppendable with specified height and ascent, without color. + * + * @param height The height of the glyph. + * @param ascent The ascent of the glyph. + * @param character The character to translate. + * @return A GlyphAppendable representing the translated character. + * @throws IllegalArgumentException If translation fails. + */ + default GlyphAppendable translate(final int height, final int ascent, final char character) + throws IllegalArgumentException { + return this.translate(height, ascent, character, null); + } + + /** + * Translates a string of text to a list of GlyphAppendable with specified height and ascent, without color. + * + * @param height The height of the glyphs. + * @param ascent The ascent of the glyphs. + * @param text The text to translate. + * @return A list of GlyphAppendable representing the translated text. + * @throws IllegalArgumentException If translation fails. + */ + default List translate(final int height, final int ascent, final String text) + throws IllegalArgumentException { + return this.translate(height, ascent, text, null); + } +} diff --git a/language/src/main/java/net/infumia/pack/ResourceProducerLanguageImpl.java b/language/src/main/java/net/infumia/pack/ResourceProducerLanguageImpl.java new file mode 100644 index 0000000..518ceb8 --- /dev/null +++ b/language/src/main/java/net/infumia/pack/ResourceProducerLanguageImpl.java @@ -0,0 +1,130 @@ +package net.infumia.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.infumia.pack.exception.ResourceAlreadyProducedException; +import net.infumia.pack.exception.ResourceNotProducedException; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +final class ResourceProducerLanguageImpl implements ResourceProducerLanguage { + + private final Key key; + private final Texture texture; + private final Map propertiesToMultichar; + + private List fontProviders; + + ResourceProducerLanguageImpl( + final Key key, + final Texture texture, + final List propertiesList, + final List charactersMapping + ) { + this.key = key; + this.texture = texture; + this.propertiesToMultichar = propertiesList + .stream() + .collect( + Collectors.toMap( + Function.identity(), + properties -> Language.multichar(key, texture, properties, charactersMapping) + ) + ); + } + + @NotNull + @Override + public Key key() { + return this.key; + } + + @Override + public boolean produced() { + return this.fontProviders != null; + } + + @Override + public void produce(final ArbitraryCharacterFactory characterFactory) + throws ResourceAlreadyProducedException { + if (this.fontProviders != null) { + throw new ResourceAlreadyProducedException(); + } + this.fontProviders = new ArrayList<>(); + this.propertiesToMultichar.values() + .forEach(multichar -> { + multichar.produce(characterFactory); + this.fontProviders.addAll(multichar.fontProviders()); + }); + } + + @Override + public Collection fontProviders() throws ResourceNotProducedException { + return this.fontProviders; + } + + @Override + public Collection textures() throws ResourceNotProducedException { + return Collections.singletonList(this.texture); + } + + private ResourceProducerImageMultichar getGlyphCollection(final int height, final int ascent) { + final TextureProperties properties = new TextureProperties(height, ascent); + final ResourceProducerImageMultichar glyphCollection = + this.propertiesToMultichar.get(properties); + if (glyphCollection == null) { + throw new IllegalArgumentException("Font with " + properties + " not found"); + } + return glyphCollection; + } + + @Override + public GlyphAppendable translate( + final int height, + final int ascent, + final char character, + final TextColor color + ) throws IllegalArgumentException { + return this.getGlyphCollection(height, ascent).translate(character, color); + } + + @Override + public List translate( + final int height, + final int ascent, + final String text, + final TextColor color + ) throws IllegalArgumentException { + return Collections.unmodifiableList( + this.getGlyphCollection(height, ascent).translate(text, color) + ); + } + + @Override + public List translate( + final int height, + final int ascent, + final Component component + ) throws IllegalArgumentException { + final ResourceProducerImageMultichar collection = this.getGlyphCollection(height, ascent); + final Collection textAndColors = Kyori.toColoredParts( + component + ); + return Collections.unmodifiableList( + textAndColors + .stream() + .map(parts -> collection.translate(parts.text(), parts.color())) + .flatMap(Collection::stream) + .collect(Collectors.toList()) + ); + } +} diff --git a/renovate.json b/renovate.json index 8f82b0a..2e0f7b2 100644 --- a/renovate.json +++ b/renovate.json @@ -1,18 +1,10 @@ { - "extends": [ - "config:base" - ], - "labels": ["dependencies"], - "schedule": [ - "after 9am" - ], - "packageRules": [ - { - "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest", "lockFileMaintenance", "rollback", "bump"], - "automerge": true - } - ], - "ignorePaths": [ - "example" - ] -} + "extends": [ "config:base" ], + "labels": [ "dependencies" ], + "schedule": [ "after 9am" ], + "packageRules": [ { + "matchUpdateTypes": [ "major", "minor", "patch", "pin", "digest", "lockFileMaintenance", "rollback", "bump" ], + "automerge": true + } ], + "ignorePaths": [ "example" ] +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5787d6e..c0670b6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} +plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } rootProject.name = "pack" + +include("common", "blank", "language", "generator")