diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81a4c2c..439d8b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,10 +39,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 - with: - gradle-version: '8.8' - - name: Build with Gradle 8.8 + - name: Build with Gradle 8.12 run: gradle -Pversion=${{steps.version.outputs.version}} build - name: Upload a Build Artifact diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ce1c62c..1fd1b27 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -10,6 +10,7 @@ + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 70fcb5c..8e55d29 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -66,5 +66,20 @@ diff --git a/build.gradle.kts b/build.gradle.kts index 101e5f6..9792fd0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,14 +4,13 @@ plugins { kotlin("jvm") version "2.0.20" java - id("com.github.johnrengelman.shadow") version "8.1.1" + id("com.gradleup.shadow") version "9.2.0" id("org.jetbrains.kotlin.plugin.serialization") version "1.5.31" id("xyz.jpenilla.run-paper") version "2.3.1" `maven-publish` - id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" - + id("io.papermc.paperweight.userdev") version "2.0.0-beta.19" } repositories { @@ -38,6 +37,7 @@ repositories { val kotlinVersion: String by properties group = "xyz.mastriel" version = properties["version"]!! +val minecraftVersion: String by properties @@ -65,8 +65,8 @@ dependencies { shadow("net.lingala.zip4j:zip4j:2.11.5") shadow("com.jhlabs:filters:2.0.235-1") - - paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + + paperweight.paperDevBundle("${minecraftVersion}-R0.1-SNAPSHOT") } @@ -75,7 +75,7 @@ tasks { runServer { pluginJars("../CuTAPI/build/libs/CuTAPI-0.1.0a-reobf.jar") - minecraftVersion("1.21.4") + minecraftVersion(minecraftVersion) } } @@ -116,7 +116,7 @@ tasks.reobfJar { } tasks.assemble { - dependsOn(tasks.reobfJar) + dependsOn(tasks.shadowJar) } publishing { diff --git a/gradle.properties b/gradle.properties index 202092a..47219cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ kotlin.code.style=official kotlinVersion=2.0.20 -version=0.1.1 \ No newline at end of file +version=0.1.1 +minecraftVersion=1.21.11 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d6e308a..d706aba 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/schemas/cutmeta-schema.json b/schemas/cutmeta-schema.json deleted file mode 100644 index a31c9c2..0000000 --- a/schemas/cutmeta-schema.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "title": "cutmeta", - "$ref": "#/definitions/cutmeta", - "definitions": { - "cutmeta": { - "description": "CuTAPI resource meta descriptions.", - "type": "object", - "properties": { - "resource_file": { - "type": "string", - "description": "The file that this .cutmeta points to. Relative paths are allowed, and $ indicates the beginning of your plugin's packFolder.\n\n__auto__ will set the file to any resource with the same name (minus extension) as this file." - }, - "recursive": { - "type": "boolean", - "description": "If this __folder__ file is recursive down the file tree." - }, - "resource_type": { - "enum": [ - "texture", - "audio", - "model" - ], - "type": "string", - "description": "The type that this resource is. 'texture' (*.png), 'audio' (*.ogg), and 'model' (*.json) are allowed." - }, - "applies_to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The minecraft items that this applies to." - }, - "used_for": { - "description": "The types of things that this texture will be used in. Only used for textures and models.", - "enum": [ - "items", - "blocks", - "any" - ], - "type": "string" - }, - "post_process": { - "description": "Post-processing for this resource. Only used for textures.", - "type": "array", - "items": { - "type": "object", - "properties": { - "processor": { - "type": "string", - "description": "A TexturePostProcessor by ID." - }, - "properties": { - "type": "object", - "description": "Additional number properties for this post-processor." - } - } - } - }, - "generate": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subfolder": { - "description": "The subfolder that this generated resource will be put in.", - "type": "string" - }, - "meta": { - "description": "The meta for this new file.", - "$ref": "#/definitions/cutmeta" - } - } - } - }, - "animation": { - "type": "object", - "properties": { - "interpolate": { - "description": "If true, Minecraft generates additional frames between frames with a frame time greater than 1 between them. Defaults to false.", - "type": "boolean" - }, - "width": { - "description": "The width of each frame. Automatically inferred if left blank.", - "type": "integer" - }, - "height": { - "description": "The height of each frame. Automatically inferred if left blank.", - "type": "integer" - }, - "frametime": { - "description": "How long each frame lasts, in ticks.", - "type": "integer" - }, - "frames": { - "description": "The order that the frames should be displayed in. Defaults to top to bottom, or [0, 1, 2, ...]", - "type": "array" - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/CuTAPI.kt b/src/main/kotlin/xyz/mastriel/cutapi/CuTAPI.kt index 917d309..59af129 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/CuTAPI.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/CuTAPI.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.cbor.* import kotlinx.serialization.json.* import net.peanuuutz.tomlkt.* import org.bukkit.plugin.Plugin +import xyz.mastriel.cutapi.CuTAPI.registerPlugin import xyz.mastriel.cutapi.block.* import xyz.mastriel.cutapi.block.breaklogic.* import xyz.mastriel.cutapi.nms.* @@ -13,7 +14,6 @@ import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.resources.minecraft.* import xyz.mastriel.cutapi.utils.* -import kotlin.collections.set /** @@ -45,6 +45,13 @@ public object CuTAPI { public val packetEventManager: PacketEventManager = PacketEventManager() internal val blockBreakManager = BlockBreakManager() + public val experimentalBlockSupport: Boolean by cutConfigValue("experimental_block_support", false) + + /** + * Most registries should probably be initialized here. + */ + public val serverReady: EventHandlerList = EventHandlerList() + /** * Register a plugin with CuTAPI. This is used to namespace any items registered with the API, and * is used to hold some other additional information about the plugin. @@ -145,7 +152,7 @@ public object CuTAPI { } - private val namespaceRegex = "[a-zA-Z0-9/_+]+".toRegex() + private val namespaceRegex = "[a-z0-9/_+.]+".toRegex() /** * Validate a namespace string, to ensure that it won't cause problems. See [registerPlugin] diff --git a/src/main/kotlin/xyz/mastriel/cutapi/CuTAPIPlugin.kt b/src/main/kotlin/xyz/mastriel/cutapi/CuTAPIPlugin.kt index 8d62f69..f183445 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/CuTAPIPlugin.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/CuTAPIPlugin.kt @@ -6,14 +6,19 @@ import com.github.shynixn.mccoroutine.bukkit.* import io.papermc.paper.plugin.lifecycle.event.types.* import kotlinx.coroutines.* import org.bukkit.* +import org.bukkit.event.* +import org.bukkit.event.server.* import org.bukkit.plugin.java.* import org.bukkit.scheduler.* +import xyz.mastriel.cutapi.CuTAPI.experimentalBlockSupport +import xyz.mastriel.cutapi.block.* import xyz.mastriel.cutapi.commands.* import xyz.mastriel.cutapi.item.* import xyz.mastriel.cutapi.item.behaviors.* import xyz.mastriel.cutapi.item.bukkitevents.* import xyz.mastriel.cutapi.item.recipe.* import xyz.mastriel.cutapi.nms.* +import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.resources.builtin.* import xyz.mastriel.cutapi.resources.minecraft.* @@ -29,6 +34,7 @@ internal lateinit var Plugin: CuTAPIPlugin @OptIn(UsesNMS::class) public class CuTAPIPlugin : JavaPlugin(), CuTPlugin { + override fun onEnable() { Plugin = this info("CuTAPI enabled!") @@ -45,23 +51,59 @@ public class CuTAPIPlugin : JavaPlugin(), CuTPlugin { registerCommands() registerEvents() registerPeriodics() + CuTItemStack.registerType( id = ItemStackUtility.DEFAULT_ITEMSTACK_TYPE_ID, kClass = CuTItemStack::class, constructor = CuTItemStack.CONSTRUCTOR ) - CustomItem.register(CustomItem.Unknown) + CustomItem.modifyRegistry(RegistryPriority(Int.MAX_VALUE)) { + register(CustomItem.Unknown) + } TexturePostProcessor.registerBuiltins() - - TexturePostProcessor.register(GrayscalePostProcessor) - TexturePostProcessor.register(PaletteSwapPostProcessor) - TexturePostProcessor.register(MultiplyOpaquePixelsProcessor) + + TexturePostProcessor.modifyRegistry { + register(GrayscalePostProcessor) + register(PaletteSwapPostProcessor) + register(MultiplyOpaquePixelsProcessor) + } + ResourcePackProcessor.register(TextureAndModelProcessor, name = "Texture Processor") - MinecraftAssetDownloader.register(GithubMinecraftAssetDownloader()) - Uploader.register(BuiltinUploader()) + + MinecraftAssetDownloader.modifyRegistry { + register(GithubMinecraftAssetDownloader()) + } + + Uploader.modifyRegistry { + register(BuiltinUploader()) + } + CuTAPI.packetEventManager.registerPacketListener(PacketItemHandler) + if (experimentalBlockSupport) CuTAPI.packetEventManager.registerPacketListener(CuTAPI.blockBreakManager) + + CustomItem.DeferredRegistry.commitToRegistry() + CuTAPI.serverReady { + ResourceFileLoader.initialize() + ResourceGenerator.initialize() + MinecraftAssetDownloader.initialize() + TexturePostProcessor.initialize() + Uploader.initialize() + + CustomItem.initialize() + CustomShapedRecipe.initialize() + CustomShapelessRecipe.initialize() + CustomFurnaceRecipe.initialize() + CustomSmithingTableRecipe.initialize() + + CustomBlock.initialize() + CustomTileEntity.initialize() + CustomTile.initialize() + + ToolCategory.initialize() + ToolTier.initialize() + } registerResourceLoaders() @@ -96,25 +138,33 @@ public class CuTAPIPlugin : JavaPlugin(), CuTPlugin { val periodicManager = CuTAPI.periodicManager periodicManager.register(this, PacketItemHandler) - // periodicManager.register(CuTAPI.blockBreakManager) + if (experimentalBlockSupport) periodicManager.register(this, CuTAPI.blockBreakManager) } private fun registerResourceLoaders() { - ResourceGenerator.register(HorizontalAtlasTextureGenerator) - - ResourceFileLoader.register(FolderApplyResourceLoader) - ResourceFileLoader.register(TemplateResourceLoader) - ResourceFileLoader.register(Texture2DResourceLoader) - ResourceFileLoader.register(Model3DResourceLoader) - ResourceFileLoader.register(MetadataResource.Loader) - ResourceFileLoader.register(PostProcessDefinitionsResource.Loader) + ResourceGenerator.modifyRegistry { + register(HorizontalAtlasTextureGenerator) + register(InventoryTextureGenerator) + } + + ResourceFileLoader.modifyRegistry { + register(TemplateResourceLoader) + register(FolderApplyResourceLoader) + register(Texture2DResourceLoader) + register(Model3DResourceLoader) + register(MetadataResource.Loader) + register(PostProcessDefinitionsResource.Loader) + register(GenerateResource.Loader) + } + + } private fun registerEvents() { server.pluginManager.registerEvents(PlayerItemEvents, this) - // server.pluginManager.registerEvents(CuTAPI.blockBreakManager, this) + if (experimentalBlockSupport) server.pluginManager.registerEvents(CuTAPI.blockBreakManager, this) server.pluginManager.registerEvents(PacketItemHandler, this) server.pluginManager.registerEvents(CraftingRecipeEvents(), this) server.pluginManager.registerEvents(Unstackable, this) @@ -122,6 +172,15 @@ public class CuTAPIPlugin : JavaPlugin(), CuTPlugin { server.pluginManager.registerEvents(UploaderJoinEvents(), this) server.pluginManager.registerEvents(CuTAPI.playerPacketManager, this) + val serverReadyHandler = object : Listener { + @EventHandler + fun serverReady(event: ServerLoadEvent) { + if (event.type != ServerLoadEvent.LoadType.STARTUP) return + CuTAPI.serverReady.trigger(Unit) + } + } + server.pluginManager.registerEvents(serverReadyHandler, this) + val itemBehaviorEvents = ItemBehaviorEvents() server.pluginManager.registerEvents(itemBehaviorEvents, this) CuTAPI.periodicManager.register(this, itemBehaviorEvents) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockDescriptor.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockDescriptor.kt index 1392c32..4bfa2aa 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockDescriptor.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockDescriptor.kt @@ -15,7 +15,6 @@ public sealed interface TileDescriptor { public val itemPolicy: BlockItemPolicy } - public class BlockDescriptor( override val behaviors: List = mutableListOf(), override val blockStrategy: BlockStrategy, diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockItemPolicy.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockItemPolicy.kt index 0d2c008..cb72eaa 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockItemPolicy.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockItemPolicy.kt @@ -15,7 +15,7 @@ import xyz.mastriel.cutapi.item.behaviors.* */ public sealed class BlockItemPolicy { - internal abstract fun tileRegister(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*>? + internal abstract fun tileCreate(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*>? /** * Generate a new item and register it. You can supply your own item descriptor which will be combined @@ -25,13 +25,13 @@ public sealed class BlockItemPolicy { public constructor(descriptor: ItemDescriptorBuilder.() -> Unit) : this(ItemDescriptorBuilder().apply(descriptor).build()) - override fun tileRegister(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*> { + override fun tileCreate(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*> { val material = when (val strategy = customTile.descriptor.blockStrategy) { is BlockStrategy.Vanilla -> strategy.material else -> Material.STONE } - if (descriptor != null) return registerCustomItem(customTile.id / "item", material, descriptor) - return registerCustomItem(customTile.id / "item", material) { + if (descriptor != null) return customItem(customTile.id / "item", material, descriptor) + return customItem(customTile.id / "item", material) { behavior(BlockPlaceBehavior(customTile)) display { @@ -47,7 +47,7 @@ public sealed class BlockItemPolicy { * If it does already have one, a warning will be printed. You shouldn't use this with an item that has one! */ public data class Item(val item: CustomItem<*>, val consumesItem: Boolean = true) : BlockItemPolicy() { - override fun tileRegister(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*> { + override fun tileCreate(tileDescriptor: TileDescriptor, customTile: CustomTile<*>): CustomItem<*> { val behaviors = item.descriptor.itemBehaviors as? MutableList ?: error("${item.id} does not have its itemBehaviors as a MutableList!") @@ -61,7 +61,7 @@ public sealed class BlockItemPolicy { * This does nothing to create relationships between your block and any items. */ public data object None : BlockItemPolicy() { - override fun tileRegister(tileDescriptor: TileDescriptor, customTile: CustomTile<*>) = null + override fun tileCreate(tileDescriptor: TileDescriptor, customTile: CustomTile<*>) = null } } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockModel.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockModel.kt index 4d4238c..5bb521a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockModel.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockModel.kt @@ -3,11 +3,13 @@ package xyz.mastriel.cutapi.block import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.resources.builtin.* +import xyz.mastriel.cutapi.resources.minecraft.* -public sealed class BlockTextures { +public interface BlockTextures { - public data class Single(val texture: ResourceRef) : BlockTextures() { + public data class Single(val texture: ResourceRef) : BlockTextures { override fun getAll(): All = All(texture, texture, texture, texture, texture, texture) + override fun getVanillaModelParent(): ResourceRef = ref(MinecraftAssets, "block/cube_all.json") } public data class All( @@ -17,25 +19,28 @@ public sealed class BlockTextures { val south: ResourceRef, val west: ResourceRef, val east: ResourceRef - ) : BlockTextures() { + ) : BlockTextures { override fun getAll(): All = this + override fun getVanillaModelParent(): ResourceRef = ref(MinecraftAssets, "block/cube.json") } public data class Column( val up: ResourceRef, val down: ResourceRef, val side: ResourceRef, - ) : BlockTextures() { + ) : BlockTextures { override fun getAll(): All = All(up, down, side, side, side, side) + override fun getVanillaModelParent(): ResourceRef = ref(MinecraftAssets, "block/cube_column.json") } - public abstract fun getAll(): All + public fun getAll(): All + public fun getVanillaModelParent(): ResourceRef } public sealed class BlockModel { public data class Cubic(val textures: BlockTextures) : BlockModel() { - internal val model: Model3D = TODO() + internal val model: Model3D = textures.getVanillaModelParent().getResource()!! } public data class Model(val model: ResourceRef) : BlockModel() { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/BlockStateDeclaration.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockStateDeclaration.kt new file mode 100644 index 0000000..6408407 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/BlockStateDeclaration.kt @@ -0,0 +1,114 @@ +package xyz.mastriel.cutapi.block + +import net.kyori.adventure.text.* +import net.kyori.adventure.text.format.* +import xyz.mastriel.cutapi.utils.* + +public abstract class BlockStateDeclaration { + + public abstract fun getPermutations(): List + public abstract fun getPermutationsCount(): Int; + + public abstract val stateName: String; + + public class Boolean(override val stateName: String) : BlockStateDeclaration() { + override fun getPermutations(): List { + return listOf(BlockStateValue.Boolean.True, BlockStateValue.Boolean.False) + } + + override fun getPermutationsCount(): Int { + return 2 + } + } + + public class Enum>( + public val enumClass: kotlin.reflect.KClass, + override val stateName: String + ) : + BlockStateDeclaration() { + + private var _cachedPermutations: List>? = null + + override fun getPermutations(): List> { + if (_cachedPermutations == null) { + _cachedPermutations = enumClass.java.enumConstants + .map { BlockStateValue.Enum(it) } + } + return _cachedPermutations!! + } + + override fun getPermutationsCount(): Int { + return enumClass.java.enumConstants.size + } + } + + public companion object { + public inline fun > Enum(name: String): Enum { + return Enum(T::class, name) + } + } +} + +public class BlockStates { + private val stateDeclarations: MutableList = mutableListOf() + + public fun add(state: BlockStateDeclaration) { + stateDeclarations += state + } + + public fun getStates(): List { + return stateDeclarations.toList() + } + + public fun getTotalPermutationsCount(): Int { + return stateDeclarations.fold(1) { acc, state -> acc * state.getPermutationsCount() } + } + + public fun createPermutations(): List> { + val permutations = mutableListOf>() + + fun backtrack( + index: Int, + currentPermutation: MutableMap + ) { + if (index == stateDeclarations.size) { + permutations.add(currentPermutation.toMap()) + return + } + + val stateDeclaration = stateDeclarations[index] + for (value in stateDeclaration.getPermutations()) { + currentPermutation[stateDeclaration] = value + backtrack(index + 1, currentPermutation) + currentPermutation.remove(stateDeclaration) + } + } + + backtrack(0, mutableMapOf()) + return permutations + } +} + +public interface BlockStateValue { + + public val displayName: Component + + public class Boolean private constructor(bool: kotlin.Boolean) : BlockStateValue { + override val displayName: Component = + bool.toString().colored.color(if (bool) NamedTextColor.GREEN else NamedTextColor.RED) + + public companion object { + public val True: Boolean = Boolean(true) + public val False: Boolean = Boolean(false) + + public fun from(value: kotlin.Boolean): Boolean { + return if (value) True else False + } + } + } + + public class Enum> public constructor(public val value: T) : BlockStateValue { + override val displayName: Component = + value.name.colored.color(NamedTextColor.AQUA) + } +} diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/CuTPlacedTile.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/CuTPlacedTile.kt index 4d61574..06fbc0c 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/CuTPlacedTile.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/CuTPlacedTile.kt @@ -2,7 +2,9 @@ package xyz.mastriel.cutapi.block import org.bukkit.* import org.bukkit.block.* +import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.pdc.tags.* +import xyz.mastriel.cutapi.registry.* public sealed class CuTPlacedTile( @@ -16,14 +18,13 @@ public sealed class CuTPlacedTile( protected abstract val typeTag: NotNullTag> + @Suppress("UNCHECKED_CAST") public var type: CustomTile<*> get() = typeTag.get() set(value) { (typeTag as NotNullTag>).store(value) } - - } @@ -31,15 +32,14 @@ public open class CuTPlacedTileEntity( handle: Block ) : CuTPlacedTile(handle) { - final override val typeTag: NotNullTag> = - customTileEntityTag("type", CustomTileEntity.Unknown) + customTileEntityTag(id(Plugin, "type"), CustomTileEntity.Unknown) } public open class CuTPlacedBlock(handle: Block) : CuTPlacedTile(handle) { - - final override val typeTag: NotNullTag> = customBlockTag("type", CustomBlock.Unknown) + final override val typeTag: NotNullTag> = + customBlockTag(id(Plugin, "type"), CustomBlock.Unknown) } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlock.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlock.kt index 2e65091..9aa7592 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlock.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlock.kt @@ -21,7 +21,7 @@ public sealed interface CustomTile : Identifiable { } public fun setAt(block: Block) { - block.tags.setIdentifier("cutapi.CuTID", id) + block.tags.setIdentifier(id(Plugin, "id"), id) when (val strategy = descriptor.blockStrategy) { is BlockStrategy.FakeEntity -> block.type = Material.BARRIER is BlockStrategy.Mushroom -> block.type = Material.RED_MUSHROOM diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlockManager.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlockManager.kt index 1a86b6f..57d0eee 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlockManager.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/CustomBlockManager.kt @@ -34,7 +34,7 @@ public class CustomBlockManager { */ private fun chunkVanillaBlocks(chunk: Chunk) = chunk.persistentDataContainer.keys - .filter { it.key.startsWith("CuTBlockData") } + .filter { it.key.startsWith("customBlockData") } .mapNotNull { namespacedKey -> val (_, x, y, z) = namespacedKey.key.split("/").map { it.toIntOrNull() } @@ -123,8 +123,8 @@ public class CustomBlockManager { ): Unit = registerPlacedTileType(id, T::class, constructor) public companion object { - public const val CUT_ID_KEY: String = "cutapi.CuTID" - public const val CUT_TYPE_KEY: String = "cutapi.CuTType" + public val CUT_ID_KEY: Identifier = id(Plugin, "id") + public val CUT_TYPE_KEY: Identifier = id(Plugin, "type") public val BukkitBlock.isCustom: Boolean diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakManager.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakManager.kt index b48738e..7617d49 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakManager.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakManager.kt @@ -1,20 +1,24 @@ package xyz.mastriel.cutapi.block.breaklogic +import com.github.shynixn.mccoroutine.bukkit.* import net.minecraft.core.* import net.minecraft.network.protocol.game.* +import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket.AttributeSnapshot import net.minecraft.world.* +import net.minecraft.world.entity.ai.attributes.* import org.bukkit.* import org.bukkit.craftbukkit.event.* import org.bukkit.entity.* import org.bukkit.event.* import org.bukkit.event.block.* +import org.bukkit.event.player.* import org.bukkit.inventory.* -import org.bukkit.potion.* import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.item.* import xyz.mastriel.cutapi.nms.* import xyz.mastriel.cutapi.periodic.* import xyz.mastriel.cutapi.utils.* +import java.lang.reflect.* import java.util.concurrent.* import java.util.logging.* @@ -22,22 +26,7 @@ import java.util.logging.* @OptIn(UsesNMS::class) public class BlockBreakManager : Listener, PacketListener { - private val breakers = ConcurrentHashMap() - - - @Periodic(1) - public fun updateEffects() { - for (player in onlinePlayers()) { - if (!player.hasPotionEffect(PotionEffectType.MINING_FATIGUE)) { - player.addPotionEffect( - PotionEffectType.MINING_FATIGUE - .createEffect(1000000, 100) - .withIcon(false) - .withParticles(false) - ) - } - } - } + private val breakers = ConcurrentHashMap() @Periodic(1) public fun updateTicks() { @@ -49,12 +38,29 @@ public class BlockBreakManager : Listener, PacketListener { breakers.remove(player) } } catch (ex: Exception) { - Plugin.logger.log(Level.SEVERE, "Error while ticking block breaker for ${breaker.player.name}", ex) + Plugin.logger.log(Level.SEVERE, "Error while ticking block breaker for ${breaker.player?.name}", ex) } } } + private fun abortBreaking(player: Player, packet: ServerboundPlayerActionPacket) { + val breaker = breakers.remove(player.playerUUID) + if (breaker != null) { + breaker.stop(false, packet.sequence) + } + } + + private fun stopBreaking(player: Player, packet: ServerboundPlayerActionPacket) { + val breaker = breakers.remove(player.playerUUID) + if (breaker != null) { + breaker.breakBlock() + breaker.stop(true) + } else { + //player.packetHandler?.injectIncoming(packet) + } + } + @PacketHandler internal fun onStartBreaking(ev: PacketEvent): ServerboundPlayerActionPacket? { val (player, packet) = ev @@ -62,23 +68,28 @@ public class BlockBreakManager : Listener, PacketListener { return when (action) { ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK -> { - startBreaking(player, packet.pos, player.activeItem, packet, packet.direction, packet.sequence) + Plugin.launch { + startBreaking(player, packet.pos, player.activeItem, packet, packet.direction, packet.sequence) + } null } ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK -> { - // stopBreaking(player, packet) + Plugin.launch { + stopBreaking(player, packet) + } null } ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK -> { - // abortBreaking(player, packet) + Plugin.launch { + abortBreaking(player, packet) + } null } else -> packet } - } private fun startBreaking( @@ -113,15 +124,70 @@ public class BlockBreakManager : Listener, PacketListener { } - val breaker = PlayerBlockBreaker(blockPosition.bukkitBlock(player.world), player, activeItem.toAgnostic()) + val breaker = + PlayerBlockBreaker( + blockPosition.bukkitBlock(player.world), + player.playerUUID, + activeItem.toAgnostic(), + sequence + ) // creative does not send stop or abort, plus we only need // to trigger the first tick since everything should break // instantly in creative mode. if (player.gameMode != GameMode.CREATIVE) { - breakers[player] = breaker + breakers[player.playerUUID] = breaker + } + + } + + @PacketHandler + private fun handleAttributes(event: PacketEvent): ClientboundUpdateAttributesPacket { + if (event.player.entityId != event.packet.entityId) + return event.packet + + val newValues = event.packet.values.map { + if (it.attribute.value() == Attributes.BLOCK_BREAK_SPEED.value()) { + AttributeSnapshot(it.attribute, 0.0, emptyList()) + } else { + it + } } + // Use reflection to access the constructor of ClientboundUpdateAttributesPacket + val ctor = ClientboundUpdateAttributesPacket::class.java.declaredConstructors + .filter { it.accessFlags().contains(AccessFlag.PRIVATE) } + .firstOrNull { it.parameterTypes.size == 2 } + ?: throw IllegalStateException("No suitable constructor found for ClientboundUpdateAttributesPacket") + + ctor.isAccessible = true + @Suppress("UNCHECKED_CAST") + return ctor.newInstance(event.packet.entityId, newValues) as ClientboundUpdateAttributesPacket + } + + @EventHandler + internal fun playerJoinEvent(event: PlayerJoinEvent) { + val attributes = listOf( + AttributeInstance(Attributes.BLOCK_BREAK_SPEED) {} + .also { + it.baseValue = 0.0 + } + ) + + val packet = ClientboundUpdateAttributesPacket( + event.player.entityId, + attributes + ) + + packet.sendTo(event.player) + + } + + @EventHandler(priority = EventPriority.LOWEST) + private fun handleQuit(event: PlayerQuitEvent) { + val player = event.player + + breakers.remove(player.playerUUID)?.stop(true) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakVisuals.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakVisuals.kt deleted file mode 100644 index ccd698e..0000000 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/BlockBreakVisuals.kt +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.mastriel.cutapi.block.breaklogic - -import xyz.mastriel.cutapi.nms.* - -/** - * This class uses NMS to send packets to display the block breaking progress. - */ -@UsesNMS -public class BlockBreakVisuals { - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/PlayerBlockBreaker.kt b/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/PlayerBlockBreaker.kt index 004f4cb..5ee4153 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/PlayerBlockBreaker.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/block/breaklogic/PlayerBlockBreaker.kt @@ -1,16 +1,35 @@ +@file:Suppress("UnstableApiUsage") + package xyz.mastriel.cutapi.block.breaklogic +import net.minecraft.core.* +import net.minecraft.network.protocol.game.* +import net.minecraft.server.* +import net.minecraft.server.level.* +import net.minecraft.world.entity.item.* +import net.minecraft.world.item.crafting.* +import net.minecraft.world.level.* +import net.minecraft.world.level.block.entity.* +import net.minecraft.world.phys.* +import net.minecraft.world.phys.shapes.* import org.bukkit.* import org.bukkit.block.* +import org.bukkit.craftbukkit.* import org.bukkit.craftbukkit.block.* import org.bukkit.craftbukkit.util.* import org.bukkit.enchantments.* import org.bukkit.entity.* +import org.bukkit.event.block.* import org.bukkit.potion.* +import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.item.* import xyz.mastriel.cutapi.item.behaviors.* import xyz.mastriel.cutapi.nms.* import xyz.mastriel.cutapi.utils.* +import kotlin.math.* +import kotlin.random.* + +private typealias MojangStack = net.minecraft.world.item.ItemStack /** * Exists only when a player is breaking a block. Controlled by the [BlockBreakManager]. @@ -22,9 +41,11 @@ import xyz.mastriel.cutapi.utils.* @UsesNMS public open class PlayerBlockBreaker( public val block: Block, - public val player: Player, - public val item: AgnosticItemStack + public val playerUUID: PlayerUUID, + public val item: AgnosticItemStack, + public val startSequence: Int, ) { + public val player: Player? by playerUUID protected val soundGroup: SoundGroup = block.blockSoundGroup protected val material: Material = block.type @@ -45,14 +66,189 @@ public open class PlayerBlockBreaker( public val isDone: Boolean get() = progress >= 1.0f + private var destroyTicks: Int = 0 + public var progress: Float = 0.0f private set(value) { field = value.coerceIn(0.0f, 1.0f) } + private fun determineBlockFace(location: Location, maxDistance: Double = 6.0): BlockFace? { + val start = Vec3(location.x, location.y, location.z) + val direction = location.direction + val end = start.add(direction.x * maxDistance, direction.y * maxDistance, direction.z * maxDistance) + + val ctx = ClipContext(start, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, CollisionContext.empty()) + val serverLevel = (location.world as CraftWorld).handle + val result = serverLevel.clip(ctx) + if (result.type == HitResult.Type.BLOCK) { + return result.direction.blockFace + } + + return null + } + + @Suppress("UnstableApiUsage") public open fun tick() { if (isDone) return + val player = player ?: return + + var damage = calculateDamageDealt() + val clientsideDamage = calculateClientsideDamage() + + if (clientsideDamage >= 1 && damage < 1) { + stop(false, startSequence) + return + } + + + val damageEvent = BlockDamageEvent( + player, + block, + determineBlockFace(player.eyeLocation) ?: BlockFace.NORTH, + item.vanilla(), + damage > 1 + ) + Plugin.server.pluginManager.callEvent(damageEvent) + if (damageEvent.isCancelled) + return + if (damageEvent.instaBreak && damage < 1) + damage = 1.0f + + if (damage <= 1.0) { + progress += damage + + // play break sound every 4 ticks + if (progress < 1.0 && destroyTicks % 4 == 0) { + val location = block.location + val world = location.world + world.playSound( + location, + soundGroup.hitSound, + SoundCategory.BLOCKS, + soundGroup.volume, + soundGroup.pitch + ) + } + + destroyTicks++ + } + if (isDone) { + breakBlock() + stop(true) + } else { + val breakStage = (progress.coerceAtMost(1.0f) * 10).toInt() + broadcastDestructionStage(block, player, breakStage) + } + } + + + private fun getVanillaBlockExp(level: ServerLevel, pos: BlockPos, tool: MojangStack): Int { + val blockState = level.getBlockState(pos) + val block = blockState.block + return block.getExpDrop(blockState, level, pos, tool, true) + } + + private fun isVanillaProtected(player: OfflinePlayer, location: Location): Boolean { + val spawnRadius = Bukkit.getServer().spawnRadius.toDouble() + val world = location.world!! + val spawnMin = world.spawnLocation.subtract(spawnRadius, 0.0, spawnRadius) + val spawnMax = world.spawnLocation.add(spawnRadius, 0.0, spawnRadius) + + fun Location.isBetweenXZ(min: Location, max: Location): Boolean = + x in min.x.rangeTo(max.x) + && z in min.z.rangeTo(max.z) + + return world.name == "world" + && spawnRadius > 0 + && !player.isOp + && location.isBetweenXZ(spawnMin, spawnMax) + } + + private fun getVanillaFurnaceExp(furnace: AbstractFurnaceBlockEntity): Int { + return furnace.recipesUsed.reference2IntEntrySet().sumOf { entry -> + val recipeHolder = MinecraftServer.getServer().recipeManager.byKey(entry.key).orElse(null) + val recipe = recipeHolder?.value as? AbstractCookingRecipe + val amount = entry.intValue + val expPerRecipe = recipe?.experience()?.toDouble() ?: 0.0 + + // Minecraft's logic to calculate the furnace exp + var exp = floor(amount * expPerRecipe).toInt() + val f = (amount * expPerRecipe) % 1 + if (f != 0.0 && Math.random() < f) { + exp++ + } + + return@sumOf exp + } + } + + private fun createBlockDropItemEntities( + level: ServerLevel, + pos: BlockPos, + items: Iterable + ): List = + items.map { + ItemEntity( + level, + pos.x + 0.5 + Random.nextDouble(-0.25, 0.25), + pos.y + 0.5 + Random.nextDouble(-0.25, 0.25), + pos.z + 0.5 + Random.nextDouble(-0.25, 0.25), + it.copy() + ).apply(ItemEntity::setDefaultPickUpDelay) + } + + private inline fun Level.captureDrops(run: () -> Unit): List { + val captureDrops = ArrayList() + this.captureDrops = captureDrops + try { + run.invoke() + return captureDrops + } finally { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + this.captureDrops = null; + } + } + + private fun broadcastDestructionStage(block: Block, player: Player, stage: Int) { + val packet = ClientboundBlockDestructionPacket(player.entityId, block.location.toBlock().nms(), stage) + MinecraftServer.getServer().playerList.broadcast( + null, + block.x.toDouble(), + block.y.toDouble(), + block.z.toDouble(), + 32.0, + block.world.nms().dimension(), + packet + ) + } + + + @Suppress("UnstableApiUsage") + public open fun breakBlock() { + val player = player ?: return + val level = (block.location.world as CraftWorld).handle + val blockPos = block.location.toBlock().nms() + + + val succeeded = player.breakBlock(block) + + if (!succeeded) { + // reset + progress = 0.0f + } + + // send ack packet + ClientboundBlockChangedAckPacket(startSequence).sendTo(player) + } + + public open fun stop(isFinished: Boolean, sequence: Int? = null) { + hasStopped = true + + if (sequence != null) { + if (player != null) ClientboundBlockChangedAckPacket(sequence).sendTo(player!!) + } } private fun isCorrectToolForDropsCustomTool(block: Block): Boolean { @@ -79,6 +275,10 @@ public open class PlayerBlockBreaker( return CraftMagicNumbers.getItem(material).isCorrectToolForDrops(nmsItem, nmsBlock) } + private fun calculateClientsideDamage(): Double { + return if (player?.gameMode == GameMode.CREATIVE) 1.0 else 0.0 + } + /** * Calculates the damage dealt to the block. * @return The damage dealt to the block, as a percentage of the block's health from 0.0f to 1.0f. @@ -86,6 +286,8 @@ public open class PlayerBlockBreaker( @Suppress("DEPRECATION") public open fun calculateDamageDealt(): Float { + val player = this.player ?: return 0f + // blocks under 0 are unbreakable if (hardness < 0f) return 0f @@ -116,7 +318,7 @@ public open class PlayerBlockBreaker( // deprecation is suppressed here because isOnGround can be // easily spoofed. it doesn't really matter that much since // it's used for a minor benefit when mining while flying. - // (which fly hacks should be pretty obvious to spot. + // (also, fly hacks should be pretty obvious to spot) if (!player.isOnGround) totalSpeedMultiplier /= 5f val correctToolIsUsed = when (item) { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/CuTItemStack.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/CuTItemStack.kt index a0847fa..e6de035 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/CuTItemStack.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/CuTItemStack.kt @@ -120,10 +120,10 @@ public open class CuTItemStack protected constructor( */ protected open fun onCreate() {} - public var type: CustomItem<*> by customItemTag("CuTID", CustomItem.Unknown) - public var nameHasChanged: Boolean by booleanTag("NameHasChanged", false) + public var type: CustomItem<*> by customItemTag(id(Plugin, "type"), CustomItem.Unknown) + public var nameHasChanged: Boolean by booleanTag(id(Plugin, "name_has_changed"), false) - internal var lore by loreTag("lore") + internal var lore by loreTag(id(Plugin, "lore")) /** diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/CustomItem.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/CustomItem.kt index 2706907..1b86895 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/CustomItem.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/CustomItem.kt @@ -84,6 +84,8 @@ public open class CustomItem( public companion object : IdentifierRegistry>("Custom Items") { + internal val DeferredRegistry = defer(RegistryPriority(Int.MAX_VALUE)) + public val Unknown: CustomItem = customItem( unknownID(), Material.ANVIL @@ -96,11 +98,11 @@ public open class CustomItem( } } - public val InventoryBackground: CustomItem = registerCustomItem( + public val InventoryBackground: CustomItem by DeferredRegistry.registerCustomItem( id = id(Plugin, "inventory_background"), Material.GLISTERING_MELON_SLICE ) { - behavior(BlankNameBehavior) + behavior(HideTooltip) display { texture = itemModel(Plugin, "ui/inventory_bg.model3d.json") @@ -144,7 +146,7 @@ public sealed class AgnosticMaterial { } return false } - + public infix fun materialIs(value: CustomItem<*>): Boolean { if (this is Custom) { return this.custom() == value diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemBuilders.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemBuilders.kt index ed16949..4e0a097 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemBuilders.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemBuilders.kt @@ -53,14 +53,13 @@ public fun typedCustomItem( } @JvmName("registerCustomItemWithStackType") -public inline fun registerCustomItem( +public inline fun DeferredRegistry>.registerCustomItem( id: Identifier, bukkitMaterial: Material, noinline block: (ItemDescriptorBuilder.() -> Unit)? -): CustomItem { - val customItem = customItem(id, bukkitMaterial, block) - CustomItem.register(customItem) - return customItem +): Deferred> { + @Suppress("UNCHECKED_CAST") + return register { customItem(id, bukkitMaterial, block) } as Deferred> } public fun customItem( @@ -90,44 +89,42 @@ public fun customItem( } } -public fun registerCustomItem( +public fun DeferredRegistry>.registerCustomItem( id: Identifier, bukkitMaterial: Material, block: ItemDescriptorBuilder.() -> Unit -): CustomItem { - val customItem = customItem(id, bukkitMaterial, block) - CustomItem.register(customItem) - return customItem +): Deferred> { + + @Suppress("UNCHECKED_CAST") + return register { customItem(id, bukkitMaterial, block) } as Deferred> } -public fun registerCustomItem( +public fun DeferredRegistry>.registerCustomItem( id: Identifier, bukkitMaterial: Material, descriptor: ItemDescriptor -): CustomItem { - val customItem = customItem(id, bukkitMaterial, descriptor) - CustomItem.register(customItem) - return customItem +): Deferred> { + @Suppress("UNCHECKED_CAST") + return register { customItem(id, bukkitMaterial, descriptor) } as Deferred> } -public fun registerCustomItem( +public fun DeferredRegistry>.registerCustomItem( id: Identifier, bukkitMaterial: Material, name: PersonalizedWithDefault -): CustomItem { - val customItem = customItem(id, bukkitMaterial, name) - CustomItem.register(customItem) - return customItem +): Deferred> { + @Suppress("UNCHECKED_CAST") + return register { customItem(id, bukkitMaterial, name) } as Deferred> } -public fun registerCustomItem( +public fun DeferredRegistry>.registerCustomItem( id: Identifier, bukkitMaterial: Material, name: PersonalizedWithDefault, behaviors: Collection -): CustomItem { +): Deferred> { val customItem = customItem(id, bukkitMaterial, name, behaviors) - CustomItem.register(customItem) - return customItem + @Suppress("UNCHECKED_CAST") + return register { customItem } as Deferred> } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemDescriptor.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemDescriptor.kt index 81eae0c..734f23f 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemDescriptor.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemDescriptor.kt @@ -114,11 +114,15 @@ public sealed class ItemTexture { public abstract fun isAvailable(): Boolean public abstract fun getRef(): ResourceRef<*> public abstract fun getItemModelId(): Identifier + public abstract val showSwapAnimation: Boolean - public data class Texture(val texture: ResourceRef) : ItemTexture() { + public data class Texture(val texture: ResourceRef, override val showSwapAnimation: Boolean) : + ItemTexture() { override fun getItemModelId(): Identifier { - return texture.getResource()?.getItemModel()?.toIdentifier() ?: unknownID() + val id = texture.getResource()?.getItemModel()?.toIdentifier() ?: unknownID() + val swapMode = if (showSwapAnimation) "__swap" else "__noswap" + return id("${id}${swapMode}") } override fun isAvailable(): Boolean = texture.isAvailable() @@ -128,12 +132,14 @@ public sealed class ItemTexture { } } - public data class Model(val model: ResourceRef) : ItemTexture() { + public data class Model(val model: ResourceRef, override val showSwapAnimation: Boolean) : ItemTexture() { override fun getItemModelId(): Identifier { - return model.getResource()?.getItemModel()?.toIdentifier() ?: unknownID() + val id = model.getResource()?.getItemModel()?.toIdentifier() ?: unknownID() + val swapMode = if (showSwapAnimation) "__swap" else "__noswap" + return id("${id}${swapMode}") } - + override fun isAvailable(): Boolean = model.isAvailable() override fun getRef(): ResourceRef<*> { @@ -142,17 +148,23 @@ public sealed class ItemTexture { } } -public fun itemTexture(texture: ResourceRef): ItemTexture.Texture = ItemTexture.Texture(texture) +public fun itemTexture(texture: ResourceRef, showSwapAnimation: Boolean = true): ItemTexture.Texture = + ItemTexture.Texture(texture, showSwapAnimation) -public fun itemModel(model: ResourceRef): ItemTexture.Model = ItemTexture.Model(model) +public fun itemModel(model: ResourceRef, showSwapAnimation: Boolean = true): ItemTexture.Model = + ItemTexture.Model(model, showSwapAnimation) -public fun itemTexture(plugin: CuTPlugin, path: String): ItemTexture.Texture = ItemTexture.Texture(ref(plugin, path)) +public fun itemTexture(plugin: CuTPlugin, path: String, showSwapAnimation: Boolean = true): ItemTexture.Texture = + ItemTexture.Texture(ref(plugin, path), showSwapAnimation) -public fun itemModel(plugin: CuTPlugin, path: String): ItemTexture.Model = ItemTexture.Model(ref(plugin, path)) +public fun itemModel(plugin: CuTPlugin, path: String, showSwapAnimation: Boolean = true): ItemTexture.Model = + ItemTexture.Model(ref(plugin, path), showSwapAnimation) -public fun itemTexture(stringPath: String): ItemTexture.Texture = ItemTexture.Texture(ref(stringPath)) +public fun itemTexture(stringPath: String, showSwapAnimation: Boolean = true): ItemTexture.Texture = + ItemTexture.Texture(ref(stringPath), showSwapAnimation) -public fun itemModel(stringPath: String): ItemTexture.Model = ItemTexture.Model(ref(stringPath)) +public fun itemModel(stringPath: String, showSwapAnimation: Boolean = true): ItemTexture.Model = + ItemTexture.Model(ref(stringPath), showSwapAnimation) /** * A class for creating dynamic displays (lore, name, etc.) for [CuTItemStack]s. diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemLore.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemLore.kt index e9fa0e6..e34942a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/ItemLore.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/ItemLore.kt @@ -5,6 +5,7 @@ import net.kyori.adventure.text.* import net.kyori.adventure.text.serializer.gson.* import xyz.mastriel.cutapi.pdc.tags.* import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* public object LoreTagConverter : TagConverter(ByteArray::class, ItemLore::class) { public val objectConverter: ObjectTagConverter = @@ -48,8 +49,8 @@ public class ItemLore { } } -public fun TagContainer.loreTag(name: String): NotNullTag = - NotNullTag(name, this, ItemLore(), LoreTagConverter) +public fun TagContainer.loreTag(id: Identifier): NotNullTag = + NotNullTag(id, this, ItemLore(), LoreTagConverter) public var CuTItemStack.displayLoreVisible: Boolean get() = lore.displayLoreVisible diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/PacketItemHandler.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/PacketItemHandler.kt index eec5a31..44989be 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/PacketItemHandler.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/PacketItemHandler.kt @@ -1,25 +1,34 @@ package xyz.mastriel.cutapi.item +import com.github.shynixn.mccoroutine.bukkit.* import com.mojang.datafixers.util.* +import kotlinx.coroutines.* import net.minecraft.core.* import net.minecraft.core.component.* +import net.minecraft.network.* import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.* import net.minecraft.network.syncher.SynchedEntityData.DataValue +import net.minecraft.world.item.* import net.minecraft.world.item.trading.* import org.bukkit.* import org.bukkit.craftbukkit.inventory.* +import org.bukkit.craftbukkit.util.* import org.bukkit.entity.* import org.bukkit.event.* import org.bukkit.event.inventory.* import org.bukkit.inventory.* +import org.bukkit.inventory.ItemStack import org.bukkit.persistence.* import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.item.ItemStackUtility.wrap import xyz.mastriel.cutapi.nms.* +import xyz.mastriel.cutapi.nms.PacketListener import xyz.mastriel.cutapi.pdc.tags.converters.* import xyz.mastriel.cutapi.periodic.* +import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.utils.* +import xyz.mastriel.cutapi.utils.personalized.* import java.util.* @@ -51,7 +60,7 @@ internal object PacketItemHandler : Listener, PacketListener { fun itemCost(mojangItemStack: MojangItemStack?): Optional { if (mojangItemStack == null) return Optional.empty() - val componentPredicate = DataComponentPredicate.allOf(mojangItemStack.components) + val componentPredicate = DataComponentExactPredicate.allOf(mojangItemStack.components) return Optional.of(ItemCost(mojangItemStack.itemHolder, mojangItemStack.count, componentPredicate)) } for (offer in event.packet.offers) { @@ -113,7 +122,7 @@ internal object PacketItemHandler : Listener, PacketListener { val hotbar = mainContents.take(9) val storedInventory = mainContents.subList(9, 36) - return listOf(craftingSlots, armorContents, storedInventory, hotbar, offhand /* cursor */) + return listOf(craftingSlots, armorContents, storedInventory, hotbar, offhand) .flatten() .map { it.nms() } .toNonNullList() @@ -150,6 +159,42 @@ internal object PacketItemHandler : Listener, PacketListener { return ClientboundSetEquipmentPacket(event.packet.entity, slots) } + private fun getServerSideStack(item: MojangItemStack): MojangItemStack { + val itemStack = item.bukkit().wrap() ?: return item + return itemStack.getPrerenderItemStack(item.count)?.nms() ?: item + } + + @PacketHandler + fun clickInventory(event: PacketEvent): ServerboundContainerClickPacket { + @Suppress("DEPRECATION") + val evilItem = + HashedStack.ActualItem(Items.DIRT.builtInRegistryHolder(), -1, HashedPatchMap(emptyMap(), emptySet())) + + // evil hack from hell + // hashedstack is evil blah blah blah + // just send something that causes a desync and forces the server to try to correct it + // not efficient however the other option involves some horrible caching + return ServerboundContainerClickPacket( + event.packet.containerId, + event.packet.stateId, + event.packet.slotNum, + event.packet.buttonNum, + event.packet.clickType, + event.packet.changedSlots, + evilItem + ) + +// return ServerboundContainerClickPacket( +// event.packet.containerId, +// event.packet.stateId, +// event.packet.slotNum, +// event.packet.buttonNum, +// event.packet.clickType, +// event.packet.changedSlots.mapValuesTo(Int2ObjectArrayMap()) { (_, item) -> getServerSideStack(item) }, +// getServerSideStack(event.packet.carriedItem) +// ) + } + @PacketHandler fun handleCreativeSetSlot(event: PacketEvent): ServerboundSetCreativeModeSlotPacket { val item = event.packet.itemStack @@ -175,6 +220,19 @@ internal object PacketItemHandler : Listener, PacketListener { } } + if (event.packet.itemStack.item == CraftMagicNumbers.getItem(Material.AIR)) { + Plugin.launch { + delay(1.ticks) + + ClientboundContainerSetSlotPacket( + 0, + 0, + event.packet.slotNum.toInt(), + net.minecraft.world.item.ItemStack.EMPTY + ).sendTo(event.player) + } + } + return ServerboundSetCreativeModeSlotPacket( event.packet.slotNum, renderIfNeeded(event.player, item.bukkit().toAgnostic()).nms() @@ -183,46 +241,7 @@ internal object PacketItemHandler : Listener, PacketListener { @Periodic(ticks = 5, asyncThread = false) fun updatePlayerItems() { - for (player in Bukkit.getOnlinePlayers().filterNotNull()) { - - // only send the packet to update the whole inventory if the player is not in creative mode - // in creative mode, the client handles the cursor slot in its entirety. we can't see what item - // is in the cursor slot at any given time since the client doesn't send that information to the server. - // https://github.com/PaperMC/Paper/issues/7797#issuecomment-1120472278 - if (player.gameMode != GameMode.CREATIVE) { - - val packet = ClientboundContainerSetContentPacket( - 0, - // having no revision/stateid works, however it sends unnecessary packets - // prs are welcome to fix this - // https://wiki.vg/Protocol#Click_Container - 0, - renderPlayerInventory(player), - customCursorItem(player).nms() - ) - - packet.sendTo(player) - } else { - // so here's what's up. we have to send a separate packet for each slot in the player's inventory - // because the client doesn't send the cursor slot to the server in creative mode. - // this stupid as fuck. - - val inventory = renderPlayerInventory(player) - - inventory.map { it.bukkit() }.forEachIndexed { index, item -> - - if (item.wrap() != null) { - val packet = ClientboundContainerSetSlotPacket( - 0, - 0, - index, - item.nms() - ) - packet.sendTo(player) - } - } - } - + for (player in onlinePlayers()) { // this is responsible for updating the inventory of the player's open window. if (player.openWindowId != null) { @@ -257,6 +276,42 @@ internal object PacketItemHandler : Listener, PacketListener { if (bottomInventory !is PlayerInventory && topInventory.type != InventoryType.CRAFTING) { sendInventory(bottomInventory) } + return + } + + // only send the packet to update the whole inventory if the player is not in creative mode + // in creative mode, the client handles the cursor slot in its entirety. we can't see what item + // is in the cursor slot at any given time since the client doesn't send that information to the server. + // https://github.com/PaperMC/Paper/issues/7797#issuecomment-1120472278 + if (player.gameMode != GameMode.CREATIVE) { + + val packet = ClientboundContainerSetContentPacket( + 0, + 0, + renderPlayerInventory(player), + customCursorItem(player).nms() + ) + + packet.sendTo(player) + } else { + // we have to send a separate packet for each slot in the player's inventory + // because the client doesn't send the cursor slot to the server in creative mode. + // this stupid as fuck. + + val inventory = renderPlayerInventory(player) + + inventory.map { it.bukkit() }.forEachIndexed { index, item -> + + if (item.wrap() != null) { + val packet = ClientboundContainerSetSlotPacket( + 0, + 0, + index, + item.nms() + ) + packet.sendTo(player) + } + } } @@ -265,7 +320,10 @@ internal object PacketItemHandler : Listener, PacketListener { private fun customCursorItem(player: Player): ItemStack { // Bukkit.broadcast("Cursor: ${player.openInventory.cursor}".colored) - player.itemOnCursor.wrap()?.withViewer(player)?.let { return it } + val item = player.itemOnCursor.wrap()?.withViewer(player) + if (item != null) { + return item + } return player.itemOnCursor } @@ -289,7 +347,7 @@ internal object PacketItemHandler : Listener, PacketListener { return ClientboundSetEntityDataPacket(packet.id, newItems) } - fun renderIfNeeded(viewer: Player, stack: AgnosticItemStack): ItemStack { + fun renderIfNeeded(viewer: Player?, stack: AgnosticItemStack): ItemStack { return when (stack) { is AgnosticItemStack.Custom -> stack.custom() withViewer viewer is AgnosticItemStack.Vanilla -> stack.vanilla() @@ -321,19 +379,19 @@ internal object PacketItemHandler : Listener, PacketListener { private val windowIds = mutableMapOf() - internal val Player.openWindowId get() = windowIds[player] + internal val Player.openWindowId get() = windowIds[this] internal fun CuTItemStack.setPrerenderItemStack(prerender: ItemStack) { - set("PrerenderItemStack", prerender.clone().also { it.amount = 1 }, ItemStackTagConverter) + set(id(Plugin, "prerenderItemStack"), prerender.clone().also { it.amount = 1 }, ItemStackTagConverter) } internal fun CuTItemStack.getPrerenderItemStack(amount: Int): ItemStack? { - return get("PrerenderItemStack", ItemStackTagConverter)?.also { it.amount = amount } + return get(id(Plugin, "prerenderItemStack"), ItemStackTagConverter)?.also { it.amount = amount } } internal fun CuTItemStack.hasPrerenderStack(): Boolean { - return has("PrerenderItemStack") + return has(id(Plugin, "prerenderItemStack")) } /* diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Equipable.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Equipable.kt new file mode 100644 index 0000000..1ff481d --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Equipable.kt @@ -0,0 +1,67 @@ +package xyz.mastriel.cutapi.item.behaviors + +import org.bukkit.inventory.* +import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.behavior.* +import xyz.mastriel.cutapi.item.* +import xyz.mastriel.cutapi.registry.* +import xyz.mastriel.cutapi.resources.* +import xyz.mastriel.cutapi.resources.builtin.* + +@RepeatableBehavior +public class Equipable private constructor( + private val slot: EquipmentSlot, + private val isSwappable: Boolean, + private val model: ResourceRef?, + private var damageItemWhenHurt: Boolean +) : ItemBehavior(id(Plugin, "equipable")) { + + public class Builder internal constructor(public val slot: EquipmentSlot) { + public var isSwappable: Boolean = true + public var model: ResourceRef? = null + public var damageItemWhenHurt: Boolean = false + } + + @Suppress("UnstableApiUsage") + override fun onCreate(item: CuTItemStack) { + item.handle.editMeta { meta -> + meta.setEquippable(meta.equippable.also { + it.slot = slot + it.model = model?.getResource()?.getItemModel()?.toIdentifier()?.toNamespacedKey() + it.isDamageOnHurt = damageItemWhenHurt + }) + } + } + + public companion object { + public fun of(slot: EquipmentSlot, builder: Builder.() -> Unit): Equipable { + val b = Builder(slot).apply(builder) + return Equipable( + slot, + b.isSwappable, + b.model, + b.damageItemWhenHurt + ) + } + + public fun head(builder: Builder.() -> Unit): Equipable = of( + EquipmentSlot.HEAD, + builder + ) + + public fun chest(builder: Builder.() -> Unit): Equipable = of( + EquipmentSlot.CHEST, + builder + ) + + public fun legs(builder: Builder.() -> Unit): Equipable = of( + EquipmentSlot.LEGS, + builder + ) + + public fun feet(builder: Builder.() -> Unit): Equipable = of( + EquipmentSlot.FEET, + builder + ) + } +} diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/BlankNameBehavior.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/HideTooltip.kt similarity index 81% rename from src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/BlankNameBehavior.kt rename to src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/HideTooltip.kt index f98a9e7..cc7ec05 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/BlankNameBehavior.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/HideTooltip.kt @@ -5,7 +5,7 @@ import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.item.* import xyz.mastriel.cutapi.registry.* -public object BlankNameBehavior : ItemBehavior(id(Plugin, "blank_name")) { +public object HideTooltip : ItemBehavior(id(Plugin, "blank_name")) { override fun onRender(viewer: Player?, item: CuTItemStack) { item.vanilla().editMeta { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/ModifyAttribute.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/ModifyAttribute.kt new file mode 100644 index 0000000..adde096 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/ModifyAttribute.kt @@ -0,0 +1,44 @@ +package xyz.mastriel.cutapi.item.behaviors + +import org.bukkit.attribute.* +import org.bukkit.inventory.* +import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.behavior.* +import xyz.mastriel.cutapi.item.* +import xyz.mastriel.cutapi.registry.* + +@Suppress("UnstableApiUsage") +@RepeatableBehavior +public class ModifyAttribute( + public val key: Identifier, + public val slotGroup: EquipmentSlotGroup, + public val attribute: Attribute, + public val amount: Double, + public val operation: AttributeModifier.Operation = AttributeModifier.Operation.ADD_NUMBER +) : ItemBehavior(id(Plugin, "attribute")) { + + override fun onCreate(item: CuTItemStack) { + updateItem(item) + } + + public fun updateItem(item: CuTItemStack) { + item.handle.editMeta { meta -> + + val previous = meta.getAttributeModifiers(attribute)?.first { it.key == key.toNamespacedKey() } + if (previous != null) { + // If the attribute modifier is already present, remove it + meta.removeAttributeModifier(attribute, previous) + } + + meta.addAttributeModifier( + attribute, + AttributeModifier( + key.toNamespacedKey(), + amount, + operation, + slotGroup + ) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Unstackable.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Unstackable.kt index 87ef151..966b3a8 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Unstackable.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/Unstackable.kt @@ -10,7 +10,7 @@ import java.util.* public object Unstackable : ItemBehavior(id(Plugin, "unstackable")), Listener { override fun onCreate(item: CuTItemStack) { - var uuid by getData(item).nullableUuidTag("UnstackableUUID") + var uuid by getData(item).nullableUuidTag(id(Plugin, "unstackable_uuid")) item.handle.amount = 1 uuid = UUID.randomUUID() } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/VanillaTool.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/VanillaTool.kt index de89a3a..b9f7cea 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/VanillaTool.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/behaviors/VanillaTool.kt @@ -4,6 +4,7 @@ import net.minecraft.core.* import net.minecraft.core.component.* import net.minecraft.core.registries.* import net.minecraft.world.level.block.* +import org.bukkit.* import org.bukkit.craftbukkit.util.* import org.bukkit.inventory.* import xyz.mastriel.cutapi.* @@ -42,7 +43,7 @@ public class VanillaTool( val minToolTier = ToolTier.fromVanillaMaterial(material) val correctToolForDrops = minToolTier.breakingLevel <= tool.tier.breakingLevel - val holder = BuiltInRegistries.BLOCK.createIntrusiveHolder(blockType) + val holder = BuiltInRegistries.BLOCK.wrapAsHolder(blockType) materialData += ToolMaterialData(holder, correctToolForDrops, special ?: tool.toolSpeed.speed) } } @@ -52,7 +53,12 @@ public class VanillaTool( override fun onCreate(item: CuTItemStack) { val nmsItem = item.vanilla().nms() - val tool = ToolComponent(rules.map { it.toRule() }, defaultMiningSpeed.speed, itemDamage) + val tool = ToolComponent( + rules.map { it.toRule() }, + defaultMiningSpeed.speed, + itemDamage, + canDestroyBlocksInCreative(item.agnosticMaterial) + ) val patch = DataComponentPatch.builder().set( DataComponents.TOOL, @@ -64,7 +70,18 @@ public class VanillaTool( item.vanilla().itemMeta = nmsItem.bukkit().itemMeta } +} +public fun canDestroyBlocksInCreative(material: AgnosticMaterial): Boolean { + return material.expectedVanillaMaterial in setOf( + Material.STONE_SWORD, + Material.IRON_SWORD, + Material.GOLDEN_SWORD, + Material.WOODEN_SWORD, + Material.DIAMOND_SWORD, + Material.NETHERITE_SWORD, + Material.COPPER_SWORD + ) } // if we don't optimize this then it becomes so fucking large that it just @@ -73,7 +90,7 @@ public class VanillaTool( @TemporaryAPI @UsesNMS private fun optimize(materialData: List): List { - val map = mutableMapOf>>() + val map = mutableMapOf>>() for (data in materialData) { val properties = ToolRuleProperties(data.correctForBlocks, data.speed) @@ -93,7 +110,7 @@ private fun optimize(materialData: List): List, + val holder: Holder, val correctForBlocks: Boolean, val speed: Float ) @@ -108,7 +125,7 @@ private data class ToolRuleProperties( @TemporaryAPI @UsesNMS private data class PlainToolComponentRule( - val blocks: List>, + val blocks: List>, val speed: Optional, val correctForDrops: Optional ) { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/CustomSmithingTableRecipe.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/CustomSmithingTableRecipe.kt index 6db1abb..21e154c 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/CustomSmithingTableRecipe.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/CustomSmithingTableRecipe.kt @@ -20,9 +20,9 @@ public class CustomSmithingTableRecipe( val recipe = SmithingTransformRecipe( id.toNamespacedKey(), result.vanilla(), - RecipeChoice.MaterialChoice(template.expectedVanillaMaterial), - RecipeChoice.MaterialChoice(base.expectedVanillaMaterial), - RecipeChoice.MaterialChoice(addition.expectedVanillaMaterial), + RecipeChoice.itemType(template.expectedVanillaMaterial.asItemType()!!), + RecipeChoice.itemType(base.expectedVanillaMaterial.asItemType()!!), + RecipeChoice.itemType(addition.expectedVanillaMaterial.asItemType()!!), ) Bukkit.addRecipe(recipe) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapedRecipeBuilder.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapedRecipeBuilder.kt index f6dcfdf..000f2ce 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapedRecipeBuilder.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapedRecipeBuilder.kt @@ -21,7 +21,8 @@ public class CustomShapedRecipeIngredient( material: Material, quantity: Int = 1, itemRequirement: Computable, - onCraft: IngredientCraftContext.() -> Unit + onCraft: IngredientCraftContext.() -> Unit, + public val placeholderItem: CustomItem<*> ) : ShapedRecipeIngredient(char, material, quantity, itemRequirement, onCraft) @@ -158,7 +159,7 @@ public class ShapedRecipeBuilder( ) { repeat(slotsRequired) { val predicate = IngredientPredicates.isItem(item) - ingredients[char] = CustomShapedRecipeIngredient(char, item.type, quantity, predicate, onCraft) + ingredients[char] = CustomShapedRecipeIngredient(char, item.type, quantity, predicate, onCraft, item) } } @@ -180,7 +181,9 @@ public fun ItemDescriptorBuilder.shapedRecipe( ) { onRegister += { val builder = ShapedRecipeBuilder(item.createItemStack(amount), id, size).apply(block) - CustomShapedRecipe.register(builder.build()) + CustomShapedRecipe.modifyRegistry { + register(builder.build()) + } } } @@ -194,9 +197,9 @@ public fun shapedRecipe( return builder.build() } -public fun registerShapedRecipe( +public fun DeferredRegistry.registerShapedRecipe( id: Identifier, size: CustomShapedRecipe.Size, result: ItemStack, block: ShapedRecipeBuilder.() -> Unit -): CustomShapedRecipe = CustomShapedRecipe.register(shapedRecipe(id, size, result, block)) \ No newline at end of file +): Deferred = register { shapedRecipe(id, size, result, block) } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapelessRecipeBuilder.kt b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapelessRecipeBuilder.kt index cd1108b..1f19fc8 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapelessRecipeBuilder.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/item/recipe/ShapelessRecipeBuilder.kt @@ -17,7 +17,8 @@ public class CustomShapelessRecipeIngredient( material: Material, quantity: Int = 1, itemRequirement: Computable, - onCraft: IngredientCraftContext.() -> Unit + onCraft: IngredientCraftContext.() -> Unit, + public val placeholderItem: CustomItem<*> ) : ShapelessRecipeIngredient(material, quantity, itemRequirement, onCraft) public data class CustomShapelessRecipe( @@ -73,7 +74,8 @@ public class ShapelessRecipeBuilder( item.type, quantity, IngredientPredicates.isItem(item), - onCraft + onCraft, + item ) } } @@ -92,7 +94,9 @@ public fun ItemDescriptorBuilder.shapelessRecipe( ) { onRegister += { val builder = ShapelessRecipeBuilder(item.createItemStack(amount), id).apply(block) - CustomShapelessRecipe.register(builder.build()) + CustomShapelessRecipe.modifyRegistry { + builder.build() + } } } @@ -105,8 +109,8 @@ public fun shapelessRecipe( return builder.build() } -public fun registerShapelessRecipe( +public fun DeferredRegistry.registerShapelessRecipe( id: Identifier, result: ItemStack, block: ShapelessRecipeBuilder.() -> Unit -): CustomShapelessRecipe = CustomShapelessRecipe.register(shapelessRecipe(id, result, block)) \ No newline at end of file +): Deferred = register { shapelessRecipe(id, result, block) } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/nms/NMSExtensions.kt b/src/main/kotlin/xyz/mastriel/cutapi/nms/NMSExtensions.kt index 17ffe6b..497b10e 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/nms/NMSExtensions.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/nms/NMSExtensions.kt @@ -1,10 +1,12 @@ package xyz.mastriel.cutapi.nms import io.netty.channel.* +import io.papermc.paper.math.* import net.minecraft.core.* import net.minecraft.server.level.* import org.bukkit.* import org.bukkit.block.* +import org.bukkit.craftbukkit.* import org.bukkit.craftbukkit.entity.* import org.bukkit.craftbukkit.inventory.* import org.bukkit.entity.* @@ -26,6 +28,17 @@ internal fun Player.nms() = (this as CraftPlayer).handle!! @UsesNMS internal fun ItemStack.nms(): MojangItemStack = CraftItemStack.asNMSCopy(this) +@UsesNMS +internal fun World.nms(): ServerLevel = (this as CraftWorld).handle!! + +@Suppress("UnstableApiUsage") +@UsesNMS +internal fun BlockPosition.nms(): BlockPos = BlockPos( + this.blockX(), + this.blockY(), + this.blockZ() +) + /** * Converts a Mojang ItemStack to a Bukkit ItemStack */ @@ -36,7 +49,7 @@ internal fun MojangItemStack.bukkit(): ItemStack = CraftItemStack.asBukkitCopy(t * Converts a collection to a non-null list */ @UsesNMS -internal fun Collection.toNonNullList(): NonNullList { +internal fun Collection.toNonNullList(): NonNullList { val list = NonNullList.create() list.addAll(this) return list @@ -47,6 +60,15 @@ internal fun MojangPacket<*>.sendTo(player: Player) { player.nms().connection.send(this) } +@UsesNMS +internal fun Player.injectIncoming(packet: MojangPacket<*>) { + val serverPlayer = this.nms() + + serverPlayer.packetPipeline().firstOrNull { it.value is PacketEventHandler } + ?.let { (it.value as PacketEventHandler).channelRead(null, packet) } + ?: error("PacketEventHandler not found in player pipeline") +} + @UsesNMS internal fun ServerPlayer.packetPipeline(): ChannelPipeline { return connection.connection.channel.pipeline() @@ -57,3 +79,12 @@ internal fun BlockPos.bukkitBlock(world: World): Block { return world.getBlockAt(x, y, z) } +public val Direction.blockFace: BlockFace + get() = when (this) { + Direction.NORTH -> BlockFace.NORTH + Direction.EAST -> BlockFace.EAST + Direction.SOUTH -> BlockFace.SOUTH + Direction.WEST -> BlockFace.WEST + Direction.UP -> BlockFace.UP + Direction.DOWN -> BlockFace.DOWN + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/PDCExtensions.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/PDCExtensions.kt index 3da39e7..109c5f4 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/PDCExtensions.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/PDCExtensions.kt @@ -1,13 +1,12 @@ package xyz.mastriel.cutapi.pdc -import org.bukkit.* import org.bukkit.persistence.* -import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.registry.* import kotlin.reflect.* @Suppress("DuplicatedCode") -internal fun PersistentDataContainer.setPrimitiveValue(primitiveClass: KClass, key: String, value: T) { - val namespacedKey = NamespacedKey(Plugin, key) +internal fun PersistentDataContainer.setPrimitiveValue(primitiveClass: KClass, id: Identifier, value: T) { + val namespacedKey = id.toNamespacedKey() when (primitiveClass) { String::class -> set(namespacedKey, PersistentDataType.STRING, value as String) ByteArray::class -> set(namespacedKey, PersistentDataType.BYTE_ARRAY, value as ByteArray) @@ -23,8 +22,8 @@ internal fun PersistentDataContainer.setPrimitiveValue(primitiveClass: } @Suppress("DuplicatedCode", "UNCHECKED_CAST") -internal fun PersistentDataContainer.getPrimitiveValue(primitiveClass: KClass, key: String) : T? { - val namespacedKey = NamespacedKey(Plugin, key) +internal fun PersistentDataContainer.getPrimitiveValue(primitiveClass: KClass, id: Identifier): T? { + val namespacedKey = id.toNamespacedKey() return when (primitiveClass) { String::class -> get(namespacedKey, PersistentDataType.STRING) ByteArray::class -> get(namespacedKey, PersistentDataType.BYTE_ARRAY) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/BlockDataTagContainer.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/BlockDataTagContainer.kt index f93040b..a33eb3b 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/BlockDataTagContainer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/BlockDataTagContainer.kt @@ -6,38 +6,39 @@ import org.bukkit.persistence.* import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.pdc.* import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* -public open class BlockDataTagContainer(public val block : Block) : TagContainer { +public open class BlockDataTagContainer(public val block: Block) : TagContainer { - public val location : Location get() = block.location - public val chunk : Chunk get() = block.chunk - public val rootContainer : PersistentDataContainer get() = block.chunk.persistentDataContainer + public val location: Location get() = block.location + public val chunk: Chunk get() = block.chunk + public val rootContainer: PersistentDataContainer get() = block.chunk.persistentDataContainer - override fun set(key: String, complexValue: C?, converter: TagConverter) { + override fun

set(id: Identifier, complexValue: C?, converter: TagConverter) { if (complexValue == null) { - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey() getOrCreateBlockData().remove(namespacedKey) return } val primitive = converter.toPrimitive(complexValue) - getOrCreateBlockData().setPrimitiveValue(converter.primitiveClass, key, primitive) + getOrCreateBlockData().setPrimitiveValue(converter.primitiveClass, id, primitive) } - override fun get(key: String, converter: TagConverter) : C? { - val namespacedKey = NamespacedKey(Plugin, key) + override fun

get(id: Identifier, converter: TagConverter): C? { + val namespacedKey = id.toNamespacedKey() val container = getOrCreateBlockData() - if (isNull(key)) storeNull(key) + if (isNull(id)) storeNull(id) if (!container.has(namespacedKey)) return null - val value = container.getPrimitiveValue(converter.primitiveClass, key) + val value = container.getPrimitiveValue(converter.primitiveClass, id) return converter.fromPrimitive(value!!) } - private fun getOrCreateBlockData() : PersistentDataContainer { + private fun getOrCreateBlockData(): PersistentDataContainer { val blockKey = getBlockKey() if (!rootContainer.has(blockKey)) { val blockDataContainer = rootContainer.adapterContext.newPersistentDataContainer() @@ -49,17 +50,17 @@ public open class BlockDataTagContainer(public val block : Block) : TagContainer ?: error("Block data doesn't exist when it should?") } - override fun has(key: String): Boolean { - return getOrCreateBlockData().has(NamespacedKey(Plugin, key)) + override fun has(id: Identifier): Boolean { + return getOrCreateBlockData().has(id.toNamespacedKey()) } - override fun isNull(key: String): Boolean { + override fun isNull(id: Identifier): Boolean { val container = chunk.persistentDataContainer - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey() return PDCTagContainer.checkNull(container, namespacedKey) } - private fun getBlockKey() : NamespacedKey { + private fun getBlockKey(): NamespacedKey { return NamespacedKey(Plugin, "CuTBlockData/${block.x}/${block.y}/${block.z}") } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemBehaviorTagContainer.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemBehaviorTagContainer.kt index 9abd260..3d6a386 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemBehaviorTagContainer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemBehaviorTagContainer.kt @@ -1,6 +1,5 @@ package xyz.mastriel.cutapi.pdc.tags -import org.bukkit.* import org.bukkit.inventory.* import org.bukkit.inventory.meta.* import org.bukkit.persistence.* @@ -11,41 +10,43 @@ import xyz.mastriel.cutapi.registry.* public class ItemBehaviorTagContainer(private val itemStack: ItemStack, componentId: Identifier) : TagContainer { - private val key = componentId.toString().replace(':', '.') + private val key = componentId - override fun

set(key: String, complexValue: C?, converter: TagConverter) { + private val behaviorsContainer = id(Plugin, "behaviors") + + override fun

set(id: Identifier, complexValue: C?, converter: TagConverter) { val meta = itemStack.itemMeta val container = getDataContainer(meta) - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey(); if (complexValue == null) return container.remove(namespacedKey) val primitiveValue = converter.toPrimitive(complexValue) - container.setPrimitiveValue(converter.primitiveClass, key, primitiveValue) + container.setPrimitiveValue(converter.primitiveClass, id, primitiveValue) setDataContainer(meta, container) itemStack.itemMeta = meta } private fun getDataContainer(meta: ItemMeta): PersistentDataContainer { - val componentsContainer = getOrCreateContainer(meta.persistentDataContainer, "CuTComponents") + val componentsContainer = getOrCreateContainer(meta.persistentDataContainer, behaviorsContainer) return getOrCreateContainer(componentsContainer, key) } public fun setDataContainer(meta: ItemMeta, container: PersistentDataContainer) { - val componentsContainer = getOrCreateContainer(meta.persistentDataContainer, "CuTComponents") - componentsContainer.set(NamespacedKey(Plugin, key), PersistentDataType.TAG_CONTAINER, container) + val componentsContainer = getOrCreateContainer(meta.persistentDataContainer, behaviorsContainer) + componentsContainer.set(key.toNamespacedKey(), PersistentDataType.TAG_CONTAINER, container) meta.persistentDataContainer.set( - NamespacedKey(Plugin, "CuTComponents"), + behaviorsContainer.toNamespacedKey(), PersistentDataType.TAG_CONTAINER, componentsContainer ) } - private fun getOrCreateContainer(container: PersistentDataContainer, key: String): PersistentDataContainer { - val namespacedKey = NamespacedKey(Plugin, key) + private fun getOrCreateContainer(container: PersistentDataContainer, id: Identifier): PersistentDataContainer { + val namespacedKey = id.toNamespacedKey() if (container.has(namespacedKey)) return container.get(namespacedKey, PersistentDataType.TAG_CONTAINER)!! val newContainer = container.adapterContext.newPersistentDataContainer() @@ -54,25 +55,25 @@ public class ItemBehaviorTagContainer(private val itemStack: ItemStack, componen } @Suppress("DuplicatedCode") - override fun

get(key: String, converter: TagConverter): C? { + override fun

get(id: Identifier, converter: TagConverter): C? { val meta = itemStack.itemMeta val container = getDataContainer(meta) - if (isNull(key)) storeNull(key) - val namespacedKey = NamespacedKey(Plugin, key) + if (isNull(id)) storeNull(id) + val namespacedKey = id.toNamespacedKey() if (!container.has(namespacedKey)) return null - val value = container.getPrimitiveValue(converter.primitiveClass, key) + val value = container.getPrimitiveValue(converter.primitiveClass, id) return converter.fromPrimitive(value!!) } - override fun has(key: String): Boolean { - return getDataContainer(itemStack.itemMeta).has(NamespacedKey(Plugin, key)) + override fun has(id: Identifier): Boolean { + return getDataContainer(itemStack.itemMeta).has(id.toNamespacedKey()) } - override fun isNull(key: String): Boolean { + override fun isNull(id: Identifier): Boolean { val container = getDataContainer(itemStack.itemMeta) - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey() return PDCTagContainer.checkNull(container, namespacedKey) } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemTagContainer.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemTagContainer.kt index 423042d..81a79d3 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemTagContainer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/ItemTagContainer.kt @@ -1,47 +1,46 @@ package xyz.mastriel.cutapi.pdc.tags -import org.bukkit.* import org.bukkit.inventory.* -import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.pdc.* import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* public open class ItemTagContainer(private val itemStack: ItemStack) : TagContainer { - override fun set(key: String, complexValue: C?, converter: TagConverter) { + override fun

set(id: Identifier, complexValue: C?, converter: TagConverter) { val meta = itemStack.itemMeta val container = meta.persistentDataContainer - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey() if (complexValue == null) return container.remove(namespacedKey) val primitiveValue = converter.toPrimitive(complexValue) - container.setPrimitiveValue(converter.primitiveClass, key, primitiveValue) + container.setPrimitiveValue(converter.primitiveClass, id, primitiveValue) itemStack.itemMeta = meta } - override fun get(key: String, converter: TagConverter) : C? { + override fun

get(id: Identifier, converter: TagConverter): C? { val meta = itemStack.itemMeta val container = meta.persistentDataContainer - if (isNull(key)) storeNull(key) - val namespacedKey = NamespacedKey(Plugin, key) + if (isNull(id)) storeNull(id) + val namespacedKey = id.toNamespacedKey() if (!container.has(namespacedKey)) return null - val value = container.getPrimitiveValue(converter.primitiveClass, key) + val value = container.getPrimitiveValue(converter.primitiveClass, id) return converter.fromPrimitive(value!!) } - override fun has(key: String): Boolean { - return itemStack.itemMeta.persistentDataContainer.has(NamespacedKey(Plugin, key)) + override fun has(id: Identifier): Boolean { + return itemStack.itemMeta.persistentDataContainer.has(id.toNamespacedKey()) } - override fun isNull(key: String): Boolean { + override fun isNull(id: Identifier): Boolean { val container = itemStack.itemMeta.persistentDataContainer - val namespacedKey = NamespacedKey(Plugin, key) + val namespacedKey = id.toNamespacedKey() return PDCTagContainer.checkNull(container, namespacedKey) } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NotNullTag.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NotNullTag.kt index 9f2cc92..52e13f8 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NotNullTag.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NotNullTag.kt @@ -1,16 +1,17 @@ package xyz.mastriel.cutapi.pdc.tags import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* import kotlin.reflect.* -public open class NotNullTag( - override val key: String, +public open class NotNullTag

( + override val key: Identifier, override var container: TagContainer, override val default: C, private val converter: TagConverter ) : Tag { - private var cachedValue : C? = null + private var cachedValue: C? = null override fun store(value: C) { container.set(key, value, converter) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NullableTag.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NullableTag.kt index 63fb822..214d32a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NullableTag.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/NullableTag.kt @@ -1,16 +1,17 @@ package xyz.mastriel.cutapi.pdc.tags import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* import kotlin.reflect.* public open class NullableTag

( - override val key: String, + override val key: Identifier, override var container: TagContainer, override val default: C?, private val converter: TagConverter ) : Tag { - private var cachedValue : C? = null + private var cachedValue: C? = null override fun store(value: C?) { if (value == null) return container.storeNull(key) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/PDCTagContainer.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/PDCTagContainer.kt index 580d150..3c9cf4b 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/PDCTagContainer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/PDCTagContainer.kt @@ -2,47 +2,44 @@ package xyz.mastriel.cutapi.pdc.tags import org.bukkit.* import org.bukkit.persistence.* -import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.pdc.* import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* public open class PDCTagContainer(public var container: PersistentDataContainer) : TagContainer { - override fun set(key: String, complexValue: C?, converter: TagConverter) { - val namespacedKey = NamespacedKey(Plugin, key) - if (complexValue == null) return container.remove(namespacedKey) + override fun

set(id: Identifier, complexValue: C?, converter: TagConverter) { + if (complexValue == null) return container.remove(id.toNamespacedKey()) val primitiveValue = converter.toPrimitive(complexValue) - container.setPrimitiveValue(converter.primitiveClass, key, primitiveValue) + container.setPrimitiveValue(converter.primitiveClass, id, primitiveValue) } - override fun get(key: String, converter: TagConverter) : C? { - val namespacedKey = NamespacedKey(Plugin, key) - if (!container.has(namespacedKey)) return null + override fun

get(id: Identifier, converter: TagConverter): C? { + if (!container.has(id.toNamespacedKey())) return null - val value = container.getPrimitiveValue(converter.primitiveClass, key) + val value = container.getPrimitiveValue(converter.primitiveClass, id) return converter.fromPrimitive(value!!) } - override fun has(key: String): Boolean { - return container.has(NamespacedKey(Plugin, key)) + override fun has(id: Identifier): Boolean { + return container.has(id.toNamespacedKey()) } - override fun isNull(key: String): Boolean { - val namespacedKey = NamespacedKey(Plugin, key) - return checkNull(container, namespacedKey) + override fun isNull(id: Identifier): Boolean { + return checkNull(container, id.toNamespacedKey()) } public companion object { - public fun checkNull(container: PersistentDataContainer, namespacedKey: NamespacedKey) : Boolean { + public fun checkNull(container: PersistentDataContainer, namespacedKey: NamespacedKey): Boolean { if (container.has(namespacedKey)) { try { return container.get(namespacedKey, PersistentDataType.STRING) == Tag.NULL - // an IllegalArgumentException is thrown if the type isn't String. + // an IllegalArgumentException is thrown if the type isn't String. } catch (ex: IllegalArgumentException) { return false } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/Tag.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/Tag.kt index 5943121..bb73977 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/Tag.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/Tag.kt @@ -1,12 +1,14 @@ package xyz.mastriel.cutapi.pdc.tags +import xyz.mastriel.cutapi.registry.* + public interface Tag { public fun store(value: T) public fun get(): T public val default: T? - public val key: String + public val key: Identifier public var container: TagContainer public companion object { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagContainer.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagContainer.kt index 9b927a5..6bacf27 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagContainer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagContainer.kt @@ -13,245 +13,262 @@ import java.util.* public interface TagContainer { - public fun

set(key: String, complexValue: C?, converter: TagConverter) + public fun

set(id: Identifier, complexValue: C?, converter: TagConverter) - public fun

get(key: String, converter: TagConverter): C? + public fun

get(id: Identifier, converter: TagConverter): C? - public fun has(key: String): Boolean + public fun has(id: Identifier): Boolean - public fun storeNull(key: String) { - set(key, Tag.NULL, PrimitiveTagConverter.String) + public fun storeNull(id: Identifier) { + set(id, Tag.NULL, PrimitiveTagConverter.String) } - public fun isNull(key: String): Boolean - - + public fun isNull(id: Identifier): Boolean } -public fun TagContainer.setPlayer(key: String, value: OfflinePlayer? = null): Unit = +public fun TagContainer.setPlayer(key: Identifier, value: OfflinePlayer? = null): Unit = set(key, value, PlayerTagConverter) -public fun TagContainer.getPlayer(key: String): OfflinePlayer? = +public fun TagContainer.getPlayer(key: Identifier): OfflinePlayer? = get(key, PlayerTagConverter) -public fun TagContainer.setString(key: String, value: String? = null): Unit = +public fun TagContainer.setString(key: Identifier, value: String? = null): Unit = set(key, value, PrimitiveTagConverter.String) -public fun TagContainer.getString(key: String): String? = +public fun TagContainer.getString(key: Identifier): String? = get(key, PrimitiveTagConverter.String) -public fun TagContainer.setInt(key: String, value: Int? = null): Unit = +public fun TagContainer.setInt(key: Identifier, value: Int? = null): Unit = set(key, value, PrimitiveTagConverter.Int) -public fun TagContainer.getInt(key: String): Int? = +public fun TagContainer.getInt(key: Identifier): Int? = get(key, PrimitiveTagConverter.Int) -public fun TagContainer.setLong(key: String, value: Long? = null): Unit = +public fun TagContainer.setLong(key: Identifier, value: Long? = null): Unit = set(key, value, PrimitiveTagConverter.Long) -public fun TagContainer.getLong(key: String): Long? = +public fun TagContainer.getLong(key: Identifier): Long? = get(key, PrimitiveTagConverter.Long) -public fun TagContainer.setFloat(key: String, value: Float? = null): Unit = +public fun TagContainer.setFloat(key: Identifier, value: Float? = null): Unit = set(key, value, PrimitiveTagConverter.Float) -public fun TagContainer.getFloat(key: String): Float? = +public fun TagContainer.getFloat(key: Identifier): Float? = get(key, PrimitiveTagConverter.Float) -public fun TagContainer.setDouble(key: String, value: Double? = null): Unit = +public fun TagContainer.setDouble(key: Identifier, value: Double? = null): Unit = set(key, value, PrimitiveTagConverter.Double) -public fun TagContainer.getDouble(key: String): Double? = +public fun TagContainer.getDouble(key: Identifier): Double? = get(key, PrimitiveTagConverter.Double) -public fun TagContainer.setBoolean(key: String, value: Boolean? = null): Unit = +public fun TagContainer.setBoolean(key: Identifier, value: Boolean? = null): Unit = set(key, value, BooleanTagConverter) -public fun TagContainer.getBoolean(key: String): Boolean? = +public fun TagContainer.getBoolean(key: Identifier): Boolean? = get(key, BooleanTagConverter) -public fun TagContainer.setUUID(key: String, value: UUID? = null): Unit = +public fun TagContainer.setUUID(key: Identifier, value: UUID? = null): Unit = set(key, value, UUIDTagConverter) -public fun TagContainer.getUUID(key: String): UUID? = +public fun TagContainer.getUUID(key: Identifier): UUID? = get(key, UUIDTagConverter) -public fun TagContainer.setIntArray(key: String, value: IntArray? = null): Unit = +public fun TagContainer.setIntArray(key: Identifier, value: IntArray? = null): Unit = set(key, value, PrimitiveTagConverter.IntArray) -public fun TagContainer.getIntArray(key: String): IntArray? = +public fun TagContainer.getIntArray(key: Identifier): IntArray? = get(key, PrimitiveTagConverter.IntArray) -public fun TagContainer.setByteArray(key: String, value: ByteArray? = null): Unit = +public fun TagContainer.setByteArray(key: Identifier, value: ByteArray? = null): Unit = set(key, value, PrimitiveTagConverter.ByteArray) -public fun TagContainer.getByteArray(key: String): ByteArray? = +public fun TagContainer.getByteArray(key: Identifier): ByteArray? = get(key, PrimitiveTagConverter.ByteArray) -public fun TagContainer.setByte(key: String, value: Byte? = null): Unit = +public fun TagContainer.setByte(key: Identifier, value: Byte? = null): Unit = set(key, value, PrimitiveTagConverter.Byte) -public fun TagContainer.getByte(key: String): Byte? = +public fun TagContainer.getByte(key: Identifier): Byte? = get(key, PrimitiveTagConverter.Byte) -public fun TagContainer.setShort(key: String, value: Short? = null): Unit = +public fun TagContainer.setShort(key: Identifier, value: Short? = null): Unit = set(key, value, PrimitiveTagConverter.Short) -public fun TagContainer.getShort(key: String): Short? = +public fun TagContainer.getShort(key: Identifier): Short? = get(key, PrimitiveTagConverter.Short) -public fun TagContainer.setIdentifier(key: String, value: Identifier? = null): Unit = +public fun TagContainer.setIdentifier(key: Identifier, value: Identifier? = null): Unit = set(key, value, IdentifierTagConverter) -public fun TagContainer.getIdentifier(key: String): Identifier? = +public fun TagContainer.getIdentifier(key: Identifier): Identifier? = get(key, IdentifierTagConverter) -public fun TagContainer.setLocation(key: String, value: Location? = null): Unit = +public fun TagContainer.setLocation(key: Identifier, value: Location? = null): Unit = set(key, value, ObjectTagConverter(Location::class, LocationSerializer)) -public fun TagContainer.getLocation(key: String): Location? = +public fun TagContainer.getLocation(key: Identifier): Location? = get(key, ObjectTagConverter(Location::class, LocationSerializer)) -public inline fun TagContainer.setResourceRef(key: String, value: ResourceRef? = null): Unit = +public inline fun TagContainer.setResourceRef( + key: Identifier, + value: ResourceRef? = null +): Unit = set(key, value, ResourceRefTagConverter()) -public inline fun TagContainer.getResourceRef(key: String): ResourceRef? = +public inline fun TagContainer.getResourceRef(key: Identifier): ResourceRef? = get(key, ResourceRefTagConverter()) -public fun TagContainer.playerTag(key: String, default: OfflinePlayer): NotNullTag = +public fun TagContainer.playerTag(key: Identifier, default: OfflinePlayer): NotNullTag = NotNullTag(key, this, default, PlayerTagConverter) -public fun TagContainer.nullablePlayerTag(key: String, default: OfflinePlayer? = null): NullableTag = +public fun TagContainer.nullablePlayerTag( + key: Identifier, + default: OfflinePlayer? = null +): NullableTag = NullableTag(key, this, default, PlayerTagConverter) -public fun TagContainer.identifierTag(key: String, default: Identifier): NotNullTag = +public fun TagContainer.identifierTag(key: Identifier, default: Identifier): NotNullTag = NotNullTag(key, this, default, IdentifierTagConverter) -public fun TagContainer.nullableIdentifierTag(key: String, default: Identifier? = null): NullableTag = +public fun TagContainer.nullableIdentifierTag( + key: Identifier, + default: Identifier? = null +): NullableTag = NullableTag(key, this, default, IdentifierTagConverter) -public fun TagContainer.customItemTag(key: String, default: CustomItem<*>): NotNullTag> = +public fun TagContainer.customItemTag(key: Identifier, default: CustomItem<*>): NotNullTag> = NotNullTag(key, this, default, IdentifiableTagConverter.CustomItem) public fun TagContainer.nullableCustomItemTag( - key: String, + key: Identifier, default: CustomItem<*>? = null ): NullableTag> = NullableTag(key, this, default, IdentifiableTagConverter.CustomItem) -public fun TagContainer.customBlockTag(key: String, default: CustomBlock<*>): NotNullTag> = +public fun TagContainer.customBlockTag(key: Identifier, default: CustomBlock<*>): NotNullTag> = NotNullTag(key, this, default, IdentifiableTagConverter.CustomBlock) public fun TagContainer.nullableCustomBlockTag( - key: String, + key: Identifier, default: CustomBlock<*>? = null ): NullableTag> = NullableTag(key, this, default, IdentifiableTagConverter.CustomBlock) -public fun TagContainer.customBlockTag(key: String, default: CustomTile<*>): NotNullTag> = +public fun TagContainer.customBlockTag(key: Identifier, default: CustomTile<*>): NotNullTag> = NotNullTag(key, this, default, IdentifiableTagConverter.CustomTile) public fun TagContainer.nullableCustomBlockTag( - key: String, + key: Identifier, default: CustomTile<*>? = null ): NullableTag> = NullableTag(key, this, default, IdentifiableTagConverter.CustomTile) public fun TagContainer.customTileEntityTag( - key: String, + key: Identifier, default: CustomTileEntity<*> ): NotNullTag> = NotNullTag(key, this, default, IdentifiableTagConverter.CustomTileEntity) public fun TagContainer.nullableTileEntityTag( - key: String, + key: Identifier, default: CustomTileEntity<*>? = null ): NullableTag> = NullableTag(key, this, default, IdentifiableTagConverter.CustomTileEntity) -public fun TagContainer.stringTag(key: String, default: String): NotNullTag = +public fun TagContainer.stringTag(key: Identifier, default: String): NotNullTag = NotNullTag(key, this, default, PrimitiveTagConverter.String) -public fun TagContainer.nullableStringTag(key: String, default: String? = null): NullableTag = +public fun TagContainer.nullableStringTag(key: Identifier, default: String? = null): NullableTag = NullableTag(key, this, default, PrimitiveTagConverter.String) -public fun TagContainer.doubleTag(key: String, default: Double): NotNullTag = +public fun TagContainer.doubleTag(key: Identifier, default: Double): NotNullTag = NotNullTag(key, this, default, PrimitiveTagConverter.Double) -public fun TagContainer.nullableDoubleTag(key: String, default: Double? = null): NullableTag = +public fun TagContainer.nullableDoubleTag(key: Identifier, default: Double? = null): NullableTag = NullableTag(key, this, default, PrimitiveTagConverter.Double) -public fun TagContainer.longTag(key: String, default: Long): NotNullTag = +public fun TagContainer.longTag(key: Identifier, default: Long): NotNullTag = NotNullTag(key, this, default, PrimitiveTagConverter.Long) -public fun TagContainer.nullableLongTag(key: String, default: Long? = null): NullableTag = +public fun TagContainer.nullableLongTag(key: Identifier, default: Long? = null): NullableTag = NullableTag(key, this, default, PrimitiveTagConverter.Long) -public fun TagContainer.intTag(key: String, default: Int): NotNullTag = +public fun TagContainer.intTag(key: Identifier, default: Int): NotNullTag = NotNullTag(key, this, default, PrimitiveTagConverter.Int) -public fun TagContainer.nullableIntTag(key: String, default: Int? = null): NullableTag = +public fun TagContainer.nullableIntTag(key: Identifier, default: Int? = null): NullableTag = NullableTag(key, this, default, PrimitiveTagConverter.Int) -public fun TagContainer.booleanTag(key: String, default: Boolean): NotNullTag = +public fun TagContainer.booleanTag(key: Identifier, default: Boolean): NotNullTag = NotNullTag(key, this, default, BooleanTagConverter) -public fun TagContainer.nullableBooleanTag(key: String, default: Boolean? = null): NullableTag = +public fun TagContainer.nullableBooleanTag(key: Identifier, default: Boolean? = null): NullableTag = NullableTag(key, this, default, BooleanTagConverter) -public fun TagContainer.uuidTag(key: String, default: UUID): NotNullTag = +public fun TagContainer.uuidTag(key: Identifier, default: UUID): NotNullTag = NotNullTag(key, this, default, UUIDTagConverter) -public fun TagContainer.nullableUuidTag(key: String, default: UUID? = null): NullableTag = +public fun TagContainer.nullableUuidTag(key: Identifier, default: UUID? = null): NullableTag = NullableTag(key, this, default, UUIDTagConverter) -public fun TagContainer.locationTag(key: String, default: Location): NotNullTag = +public fun TagContainer.locationTag(key: Identifier, default: Location): NotNullTag = objectTag(key, default, LocationSerializer) -public fun TagContainer.nullableLocationTag(key: String, default: Location? = null): NullableTag = +public fun TagContainer.nullableLocationTag( + key: Identifier, + default: Location? = null +): NullableTag = nullableObjectTag(key, default, LocationSerializer) -public inline fun > TagContainer.enumTag(key: String, default: T): NotNullTag = +public inline fun > TagContainer.enumTag(key: Identifier, default: T): NotNullTag = NotNullTag(key, this, default, EnumTagConverter(T::class)) -public inline fun > TagContainer.nullableEnumTag(key: String, default: T? = null): NullableTag = +public inline fun > TagContainer.nullableEnumTag( + key: Identifier, + default: T? = null +): NullableTag = NullableTag(key, this, default, EnumTagConverter(T::class)) public inline fun TagContainer.refTag( - key: String, + key: Identifier, default: ResourceRef ): NotNullTag> = NotNullTag(key, this, default, ResourceRefTagConverter()) public inline fun TagContainer.nullableRefTag( - key: String, + key: Identifier, default: ResourceRef? = null ): NullableTag> = NullableTag(key, this, default, ResourceRefTagConverter()) public inline fun TagContainer.objectTag( - key: String, + key: Identifier, default: T, serializer: KSerializer ): NotNullTag = NotNullTag(key, this, default, ObjectTagConverter(T::class, serializer)) public inline fun TagContainer.nullableObjectTag( - key: String, + key: Identifier, default: T?, serializer: KSerializer ): NullableTag = NullableTag(key, this, default, ObjectTagConverter(T::class, serializer)) -public inline fun > TagContainer.setEnum(key: String, value: T? = null): Unit = +public inline fun > TagContainer.setEnum(key: Identifier, value: T? = null): Unit = set(key, value, EnumTagConverter(T::class)) -public inline fun > TagContainer.getEnum(key: String): T? = +public inline fun > TagContainer.getEnum(key: Identifier): T? = get(key, EnumTagConverter(T::class)) -public inline fun TagContainer.setObject(key: String, value: T? = null, serializer: KSerializer): Unit = +public inline fun TagContainer.setObject( + key: Identifier, + value: T? = null, + serializer: KSerializer +): Unit = set(key, value, ObjectTagConverter(T::class, serializer)) -public inline fun TagContainer.getObject(key: String, serializer: KSerializer): T? = +public inline fun TagContainer.getObject(key: Identifier, serializer: KSerializer): T? = get(key, ObjectTagConverter(T::class, serializer)) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagRef.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagRef.kt new file mode 100644 index 0000000..146207b --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/TagRef.kt @@ -0,0 +1,50 @@ +package xyz.mastriel.cutapi.pdc.tags + +import kotlinx.serialization.* +import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.pdc.tags.converters.* +import xyz.mastriel.cutapi.registry.* + +/** + * A reference to a tag of type T. + */ +public interface TagRef { + public val id: Identifier + + public val converter: TagConverter<*, T> +} + +private class TagRefImpl( + override val id: Identifier, + override val converter: TagConverter<*, T> +) : TagRef { + +} + +public fun > tagRef(id: Identifier, type: C): TagRef { + return TagRefImpl(id, type) +} + +public fun > tagRef(namespace: CuTPlugin, name: String, type: C): TagRef { + return tagRef(id(namespace, name), type) +} + +public inline fun objectTagRef( + namespace: CuTPlugin, + name: String, + serializer: KSerializer +): TagRef { + return tagRef(id(namespace, name), ObjectTagConverter(T::class, serializer)) +} + +public fun TagRef.toIdentifier(): Identifier { + return this.id +} + +public fun TagContainer.get(tagRef: TagRef): T? { + return this.get(tagRef.id, tagRef.converter) +} + +public fun TagContainer.set(tagRef: TagRef, value: T?) { + this.set(tagRef.id, value, tagRef.converter) +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/BooleanTagConverter.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/BooleanTagConverter.kt index 29850e0..416b293 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/BooleanTagConverter.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/BooleanTagConverter.kt @@ -4,7 +4,7 @@ public object BooleanTagConverter : TagConverter(Byte::class, Boolean::class) { override fun fromPrimitive(primitive: Byte): Boolean { - return primitive == 1.toByte() + return primitive != 0.toByte() } override fun toPrimitive(complex: Boolean): Byte { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/ItemStackTagConverter.kt b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/ItemStackTagConverter.kt index 45e18ac..556ec3a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/ItemStackTagConverter.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/pdc/tags/converters/ItemStackTagConverter.kt @@ -2,7 +2,6 @@ package xyz.mastriel.cutapi.pdc.tags.converters import kotlinx.serialization.* import org.bukkit.inventory.* -import org.bukkit.util.io.* import java.io.* @@ -10,11 +9,7 @@ public object ItemStackTagConverter : TagConverter(ByteArr override fun fromPrimitive(primitive: ByteArray): ItemStack { try { - ByteArrayInputStream(primitive).use { inputStream -> - BukkitObjectInputStream(inputStream).use { bukkitObjectInputStream -> - return bukkitObjectInputStream.readObject() as ItemStack - } - } + return ItemStack.deserializeBytes(primitive) } catch (e: IOException) { throw SerializationException("Failed to deserialize ItemStack", e) } catch (e: ClassNotFoundException) { @@ -24,12 +19,7 @@ public object ItemStackTagConverter : TagConverter(ByteArr override fun toPrimitive(complex: ItemStack): ByteArray { try { - ByteArrayOutputStream().use { outputStream -> - BukkitObjectOutputStream(outputStream).use { bukkitObjectOutputStream -> - bukkitObjectOutputStream.writeObject(complex) - return outputStream.toByteArray() - } - } + return complex.serializeAsBytes() } catch (e: IOException) { throw SerializationException("Failed to serialize ItemStack", e) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/registry/DeferredRegistry.kt b/src/main/kotlin/xyz/mastriel/cutapi/registry/DeferredRegistry.kt new file mode 100644 index 0000000..d201fa5 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/registry/DeferredRegistry.kt @@ -0,0 +1,101 @@ +package xyz.mastriel.cutapi.registry + +import kotlin.reflect.* + + +public interface Deferred { + public operator fun getValue(thisRef: Any?, property: KProperty<*>): T + + public fun get(): T + + public fun hasEarlyInit(): Boolean = false +} + + +public class SingleProducer(private val producer: () -> T) { + private var value: T? = null + public fun produce(): T { + if (value == null) value = producer() + return value!!; + } +} + +public class DeferredDelegate internal constructor( + private val idRegistry: IdentifierRegistry, + private val deferredRegistry: DeferredRegistry, + private val producer: SingleProducer +) : Deferred { + internal var id: Identifier? = null + public override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return get(); + } + + public override fun get(): T { + return producer.produce() + } +} + + +public interface DeferredRegistry { + public val isOpen: Boolean + + /** + * Registers a deferred item. Note that this must return an Identifiable with a consistent Identifier. + */ + public fun register(producer: () -> T): Deferred + public fun getByProducer(producer: () -> T): Identifier + public fun associateId(producer: () -> T, id: Identifier) + public fun commitToRegistry() +} + +public open class BasicDeferredRegistry internal constructor( + private val registry: IdentifierRegistry, + private val priority: RegistryPriority +) : DeferredRegistry { + protected data class DeferredItem( + val producer: SingleProducer, + val delegate: DeferredDelegate + ) + + private val items: MutableList> = mutableListOf() + private val producersToIds = mutableMapOf, Identifier>() + final override var isOpen: Boolean = true + protected set + + /** + * Registers a deferred item. Note that this must return an Identifiable with a consistent Identifier. + */ + override fun register(producer: () -> T): Deferred { + if (!isOpen) error("Deferred registry is already closed") + val single = SingleProducer(producer) + return DeferredDelegate(registry, this, single).also { + items += DeferredItem(single, it) + } + } + + override fun getByProducer(producer: () -> T): Identifier { + val single = SingleProducer(producer) + return producersToIds[single] ?: error("Producer not registered") + } + + override fun associateId(producer: () -> T, id: Identifier) { + val single = SingleProducer(producer) + producersToIds[single] = id + } + + + override fun commitToRegistry() { + if (!isOpen) error("Deferred registry is already closed") + isOpen = false + + // Register all items in the registry + registry.modifyRegistry(priority) { + for ((producer, delegate) in items) { + + val item = producer.produce() + delegate.id = item.id + register(item) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifiable.kt b/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifiable.kt index 3dcc0ff..59790a2 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifiable.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifiable.kt @@ -1,12 +1,8 @@ package xyz.mastriel.cutapi.registry -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Polymorphic -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* /** @@ -16,18 +12,71 @@ public interface Identifiable { public val id: Identifier } +public sealed class SerialDefault { + public class None() : SerialDefault() + public data class Some(val value: T) : SerialDefault() +} + -public open class IdentifiableSerializer(public val serialName: String, public val map: IdentifierRegistry) : KSerializer { +public open class IdentifiableSerializer( + public val serialName: String, + public val map: IdentifierRegistry, + public val default: SerialDefault = SerialDefault.None() +) : KSerializer { override val descriptor: SerialDescriptor get() = PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): T { val identifier = IdentifierSerializer.deserialize(decoder) - return map.get(identifier) + return when (val value = map.getOrNull(identifier)) { + null -> when (default) { + is SerialDefault.None -> throw SerializationException("Identifier $identifier not found in registry $serialName") + is SerialDefault.Some -> default.value + } + + else -> value + } } override fun serialize(encoder: Encoder, value: T) { val identifier = value.id.toString() encoder.encodeString(identifier) } +} + +public open class NullableIdentifiableSerializer( + public val serialName: String, + public val map: IdentifierRegistry, + public val default: SerialDefault = SerialDefault.None() +) : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING) + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): T? { + val identifier = IdentifierSerializer.deserialize(decoder) + + if (identifier.toString() == "cutapi:null") { + return when (default) { + is SerialDefault.None -> null + is SerialDefault.Some -> default.value + } + } + + return when (val value = map.getOrNull(identifier)) { + null -> when (default) { + is SerialDefault.None -> null + is SerialDefault.Some -> default.value + } + + else -> value + } + } + + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: T?) { + if (value == null) return encoder.encodeString("cutapi:null") + val identifier = value.id.toString() + encoder.encodeString(identifier) + } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifier.kt b/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifier.kt index 58fb05b..68d92eb 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifier.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/registry/Identifier.kt @@ -57,15 +57,16 @@ public fun NamespacedKey.toIdentifier(): Identifier = id("$namespace:$key") * * @param plugin The plugin used for the namespace * @param id This must follow the same naming rules as namespaces. See [CuTAPI.registerPlugin] for more info. + * This will be automatically made to lowercase. * * @see CuTAPI.registerPlugin */ public fun id(plugin: CuTPlugin, id: String): Identifier { CuTAPI.requireRegistered(plugin) - CuTAPI.requireValidNamespace(id) + CuTAPI.requireValidNamespace(id.lowercase()) val namespace = CuTAPI.getDescriptor(plugin).namespace - return Identifier(namespace, id) + return Identifier(namespace, id.lowercase()) } /** @@ -77,7 +78,7 @@ public fun id(plugin: CuTPlugin, id: String): Identifier { */ public fun id(stringRepresentation: String): Identifier { require(":" in stringRepresentation) { "String identifier $stringRepresentation does not follow namespace:id format." } - val (namespace, id) = stringRepresentation.split(":", limit = 2) + val (namespace, id) = stringRepresentation.lowercase().split(":", limit = 2) return Identifier(namespace, id) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/registry/IdentifierRegistry.kt b/src/main/kotlin/xyz/mastriel/cutapi/registry/IdentifierRegistry.kt index b082a3e..6f52cee 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/registry/IdentifierRegistry.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/registry/IdentifierRegistry.kt @@ -20,22 +20,109 @@ public data class HookContext( var preventRegister: Boolean ) +public data class RegistryEvent( + public val registry: IdentifierRegistry<*>, + private val registerFunction: (item: T) -> Unit, + private val replaceFunction: (id: Identifier, item: T) -> Unit +) { + public fun register(item: T): Unit = registerFunction(item) + public fun replace(id: Identifier, item: T): Unit = replaceFunction(id, item) +} + +@JvmInline +public value class RegistryPriority(public val value: Int) : Comparable { + public companion object { + public val High: RegistryPriority = RegistryPriority(1000) + public val Medium: RegistryPriority = RegistryPriority(0) + public val Low: RegistryPriority = RegistryPriority(-1000) + } + + override fun compareTo(other: RegistryPriority): Int { + return this.value.compareTo(other.value) + } +} + /** * A map of [Identifier] to [T]. This is used in keeping a registry of all items, blocks, etc. * * @param T The identifiable that is being tracked. */ public open class IdentifierRegistry(public val name: String) { + + protected data class Handler( + public val priority: RegistryPriority, + public val handler: RegistryEvent.() -> Unit + ) + protected val values: MutableMap = mutableMapOf() protected val hooks: MutableList.() -> Unit, HookPriority>> = mutableListOf() + protected val eventHandlers: MutableList> = mutableListOf() + + protected fun getSortedEventHandlers(): List> { + return eventHandlers.sortedByDescending { it.priority } + } + + public var isOpen: Boolean = true + private set + + public open fun modifyRegistry( + priority: RegistryPriority = RegistryPriority.Medium, + handler: RegistryEvent.() -> Unit + ) { + if (!isOpen) Plugin.warn("Registry '$name' is already initialized. Cannot add more handlers.") + eventHandlers += Handler(priority, handler) + } + + + /** + * Call this when the registry is ready to be used, and all handlers have already been added. + */ + public open fun initialize() { + if (!isOpen) Plugin.warn("Registry '$name' is already initialized.") + + // run all handlers + for (handler in getSortedEventHandlers()) { + val event = RegistryEvent(this, ::register, ::replace) + handler.handler(event) + } + + isOpen = false + Plugin.info("[REGISTRY] '$name' initialized with ${values.size} items.") + eventHandlers.clear() // we don't need to keep the handlers around anymore + } + + protected open fun replace(id: Identifier, item: T) { + if (!isOpen) Plugin.warn("Registry '$name' is already initialized. Cannot replace items.") + + if (values.containsKey(id)) { + unregister(id) + } else { + Plugin.warn("[REGISTRY] $id tried to be replaced in '$name', but it doesn't exist in this registry. Creating a new entry instead...") + } + register(item) + } + + /** + * Create a deferred registry for this registry. This allows you to group items together, to then be registered + * all at the same time when the registry is ready. + * + * @param priority The priority of the deferred registry. Defaults to [RegistryPriority.Medium]. + * @return A [DeferredRegistry] that can be used to register items later. + */ + public open fun defer(priority: RegistryPriority = RegistryPriority.Medium): DeferredRegistry { + if (!isOpen) Plugin.warn("Registry '$name' is already initialized. Cannot create a deferred registry.") + return BasicDeferredRegistry(this, priority) + } + + /** * Register an object with this map to allow for it to be identified. * * @param item The object which the association is being made for. */ - public open fun register(item: T): T { + protected open fun register(item: T): T { // add this to the list of used registries if it's not already there. keeps track of all registries // for when a plugin is disabled. if (!usedRegistries.any { it.get() == this }) { @@ -71,7 +158,7 @@ public open class IdentifierRegistry(public val name: String) * * @param item The object being removed. */ - public open fun unregister(item: T): Unit = unregister(item.id) + protected open fun unregister(item: T): Unit = unregister(item.id) /** * Remove an object from this registry. @@ -86,7 +173,7 @@ public open class IdentifierRegistry(public val name: String) * * @param id The object being removed. */ - public open fun unregister(id: Identifier) { + protected open fun unregister(id: Identifier) { if (!values.containsKey(id)) { Plugin.warn("[REGISTRY] $id tried to be removed from '${this.name}', but it doesn't exist in this registry.") return @@ -164,7 +251,7 @@ public open class IdentifierRegistry(public val name: String) hooks += func to priority } - internal companion object { + public companion object { // uses weak references, although registries should probably not be // garbage collected at any point and should always have a strong reference private val usedRegistries = mutableListOf>>() @@ -176,3 +263,4 @@ public open class IdentifierRegistry(public val name: String) } } } + diff --git a/src/main/kotlin/xyz/mastriel/cutapi/registry/SealedObjectReigstry.kt b/src/main/kotlin/xyz/mastriel/cutapi/registry/SealedObjectReigstry.kt index 6a74f32..ab42ab5 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/registry/SealedObjectReigstry.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/registry/SealedObjectReigstry.kt @@ -38,7 +38,7 @@ public open class SealedObjectRegistry(name: String, sealedCla registerClass(subclass) } } else { - if (kClass.objectInstance == null) { + if (!hasObjectInstance(kClass)) { Plugin.logger.warning("There's no object instance for ${kClass.simpleName} in a '${name}'.") } else { register(kClass.objectInstance!!) @@ -46,6 +46,14 @@ public open class SealedObjectRegistry(name: String, sealedCla } } + private fun hasObjectInstance(kClass: KClass): Boolean { + return try { + kClass.objectInstance != null + } catch (ex: Exception) { + false + } + } + /** * You cannot register after this class has finished initializing. This is to keep * the promise of not containing any other objects than that of the sealed subclass. diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/FolderRef.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/FolderRef.kt index 36e6c93..b4b65fb 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/FolderRef.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/FolderRef.kt @@ -2,17 +2,32 @@ package xyz.mastriel.cutapi.resources import xyz.mastriel.cutapi.* +/** + * Represents a reference to a folder within a [ResourceRoot]. + * + * @property root The root resource container this folder belongs to. + * @property pathList The path segments of the folder within the root. + */ @ConsistentCopyVisibility public data class FolderRef internal constructor( override val root: ResourceRoot, override val pathList: List ) : Locator { + /** + * Returns true if this folder is the root folder. + */ val isRoot: Boolean get() = pathList.isEmpty() + /** + * The path of this folder as a string, ending with a '/' if not root. + */ override val path: String get() = if (pathList.isEmpty()) "" else pathList.joinToString("/") + "/" + /** + * The parent folder reference, or null if this is the root. + */ override val parent: FolderRef? get() { val list = pathList.dropLast(1) @@ -20,38 +35,73 @@ public data class FolderRef internal constructor( return folderRef(root, list.joinToString("/")) } + /** + * Returns true if this folder has any children (files or folders). + */ public fun hasChildren(): Boolean { return getChildren().isNotEmpty() } + /** + * Returns a list of all children (files and folders) in this folder. + */ public fun getChildren(): List { return CuTAPI.resourceManager.getFolderContents(root, this) } + /** + * Returns the string representation of this folder reference in the format 'namespace://path/'. + */ override fun toString(): String { return "${root.namespace}://${path}" } + /** + * Returns a new [FolderRef] by appending the given path to this folder. + * + * @param path The path to append, split by '/'. + */ public operator fun div(path: String): FolderRef { return FolderRef(root, pathList + path.split("/")) } + /** + * Appends the given path to this folder and returns a new [FolderRef]. + * + * @param path The path to append. + */ public fun append(path: String): FolderRef { return this / path } + /** + * Returns a [ResourceRef] to a child resource with the given path. + * + * @param path The path to the child resource. + */ public fun child(path: String): ResourceRef { return ResourceRef(root, pathList + path.removePrefix("/").removeSuffix("/").split("/")) } } +/** + * Creates a [FolderRef] from a [ResourceRoot] and a path string. + * + * @param root The resource root. + * @param path The folder path. + * @return The [FolderRef] for the given root and path. + */ public fun folderRef(root: ResourceRoot, path: String): FolderRef { if (path == "") return FolderRef(root, emptyList()) return FolderRef(root, normalizeFolder(path).split("/").filterNot { it.isEmpty() }) } /** + * Creates a [FolderRef] from a string in the format 'namespace://path/'. + * + * @param stringPath The string path. * @throws IllegalStateException if the root is not found. + * @return The [FolderRef] for the given string path. */ public fun folderRef(stringPath: String): FolderRef { val (root, path) = stringPath.split("://") @@ -62,6 +112,13 @@ public fun folderRef(stringPath: String): FolderRef { return folderRef(resourceRoot, normalizeFolder(path)) } +/** + * Normalizes a folder path to use forward slashes, removes leading slashes, + * and ensures the path ends with a '/'. + * + * @param path The folder path to normalize. + * @return The normalized folder path. + */ public fun normalizeFolder(path: String): String { if (path.isEmpty()) return "" var newPath = path.replace("\\", "/") diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/Locator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/Locator.kt index 3976e9c..696aa9e 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/Locator.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/Locator.kt @@ -3,36 +3,76 @@ package xyz.mastriel.cutapi.resources import xyz.mastriel.cutapi.* import java.io.* +/** + * Represents a location of a resource or folder within a resource root. + * Provides access to the resource's file, root, plugin, namespace, path, and parent. + */ public sealed interface Locator { /** - * The physical file in the tmp-resource folder for this resource. This may be null! - * This should only really be used when initally loading resources, and this will probably - * be already taken care of for you. + * Returns the physical file in the tmp-resource folder for this resource, or null if not applicable. + * This is mainly used during initial resource loading. + * + * @return The associated [File], or null if not available. */ public fun getFile(): File? = null + + /** + * The root resource container this locator belongs to. + */ public val root: ResourceRoot + /** + * The plugin that owns this locator. + */ public val plugin: CuTPlugin get() = root.cutPlugin + /** + * The namespace of this locator. + */ public val namespace: String get() = root.namespace /** - * The path of this resource/folder without a namespace. Useful for things like filesystem pathing. + * The path of this resource or folder, without the namespace. + * Useful for filesystem pathing. */ public val path: String /** - * A list of the path split at /. Includes the file name, or the current folder name in a [FolderRef] + * The path split at '/', including the file or folder name. */ public val pathList: List + + /** + * The parent folder reference, or null if this is the root. + */ public val parent: FolderRef? get() = null + /** + * Returns the namespaced path representation of this locator. + * + * @return The namespaced path as a [String]. + */ public fun toNamespacedPath(): String = toString() public companion object { + /** + * Separator for root in resource paths. + */ public const val ROOT_SEPARATOR: String = "@" + + /** + * Separator for generated resources in paths. + */ public const val GENERATED_SEPARATOR: String = "^" + + /** + * Separator for cloned resources in paths. + */ public const val CLONE_SEPARATOR: String = "+" + + /** + * Separator for subresources in paths. + */ public const val SUBRESOURCE_SEPARATOR: String = "#" } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/Resource.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/Resource.kt index 5eee608..1a9af8a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/Resource.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/Resource.kt @@ -4,18 +4,34 @@ import kotlinx.serialization.* import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.resources.data.* import java.io.* +import kotlin.contracts.* +@OptIn(ExperimentalSerializationApi::class) +/** + * Represents a resource in the CuTAPI system. + * + * @property ref The reference to this resource. + * @property metadata The metadata associated with this resource, if any. + */ public open class Resource( public open val ref: ResourceRef<*>, public open val metadata: CuTMeta? = null ) { + /** + * Inspector for this resource, used for debugging and introspection. + */ public val inspector: ResourceInspector = ResourceInspector() + /** + * Checks if this resource is a subresource (contains '#' in its name). + * + * @return true if this is a subresource, false otherwise. + */ public fun isSubresource(): Boolean = "#" in ref.name init { - inspector.single("Resource Type") { this::class.simpleName ?: "Unknown" } + inspector.single("Resource Type") { this::class.simpleName ?: "" } inspector.single("Is Serializable") { this is ByteArraySerializable } inspector.single("Plugin") { ref.plugin.namespace } inspector.map("Subresources") { @@ -23,8 +39,14 @@ public open class Resource( } } + /** + * The root resource container for this resource. + */ public val root: ResourceRoot get() = ref.root + /** + * The plugin that owns this resource. + */ public val plugin: CuTPlugin get() = root.cutPlugin /** @@ -33,10 +55,30 @@ public open class Resource( public open var subResources: List = emptyList() protected set + /** + * Adds a subresource to this resource. + * + * @param resource The subresource to add. + * @throws IllegalArgumentException if the subresource does not share the same root. + */ + protected fun subResource(resource: Resource) { + if (resource.ref.root != this.ref.root) { + throw IllegalArgumentException("Sub-resource must have the same root as the parent resource.") + } + subResources = subResources + resource + } + // these are set by the resource manager // these are also purely for the clone block // and processing cloning resources. + /** + * The file from which this resource was loaded, if any. + */ internal var loadedFromFile: File? = null + + /** + * The metadata file from which this resource was loaded, if any. + */ internal val loadedFromMetadata: File? get() = if (loadedFromFile == null) null else File(loadedFromFile?.absolutePath + ".meta") @@ -50,7 +92,6 @@ public open class Resource( */ public open fun check() {} - /** * Called immediately when the resource is registered. * @@ -58,28 +99,56 @@ public open class Resource( */ public open fun onRegister() {} - /** * Throws a [ResourceCheckException] if a specified [ResourceRef] is not available. + * + * @param ref The resource reference to check. + * @param lazyReason Optional lambda to provide a reason for the exception. + * @throws ResourceCheckException if the reference is not available. */ protected fun requireValidRef(ref: ResourceRef<*>, lazyReason: (() -> String)? = null) { if (!ref.isAvailable()) throw ResourceCheckException(lazyReason?.invoke()) } } - +/** + * Exception thrown when a resource fails a logical check. + * + * @param reason The reason for the failure. + */ public class ResourceCheckException(reason: String?) : Exception(reason) +/** + * Interface for resources that can be serialized to a byte array. + */ public interface ByteArraySerializable { + /** + * Serializes the resource to a byte array. + * + * @return The serialized byte array. + */ public fun toBytes(): ByteArray } +/** + * Saves a serializable resource to a file. + * + * @receiver The resource to save. + * @param file The file to save to. + */ public fun T.saveTo(file: File) where T : ByteArraySerializable, T : Resource { file.parentFile.mkdirs() if (!file.exists()) file.createNewFile() file.writeBytes(toBytes()) } +/** + * Saves a serializable resource and its metadata to files. + * + * @receiver The resource to save. + * @param file The file to save the resource to. + * @param metadataSerializer The serializer for the metadata, or null to use the default. + */ public fun T.saveWithMetadata( file: File, metadataSerializer: KSerializer? = null @@ -96,14 +165,38 @@ public fun T.saveWithMetadata( saveTo(file) } -public fun Resource.isSerializable(): Boolean = this is ByteArraySerializable - +/** + * Checks if a resource is serializable as a [ByteArraySerializable]. + * + * @receiver The resource to check. + * @return true if the resource is serializable, false otherwise. + */ +@OptIn(ExperimentalContracts::class) +public fun Resource.isSerializable(): Boolean { + contract { + returns(true) implies (this@isSerializable is ByteArraySerializable) + } + return this is ByteArraySerializable +} -@OptIn(ExperimentalSerializationApi::class) +/** + * Serializes a resource to CBOR format using the provided serializer. + * + * @receiver The resource to serialize. + * @param serializer The serializer for the resource type. + * @return The CBOR-encoded byte array. + */ public fun T.cborSerialize(serializer: KSerializer): ByteArray { return CuTAPI.cbor.encodeToByteArray(serializer, this) } +/** + * Creates a subresource reference from this resource reference. + * + * @receiver The parent resource reference. + * @param refName The name of the subresource. + * @return The subresource reference. + */ public fun ResourceRef<*>.subRef(refName: String): ResourceRef { return ref(this.root, "${this.path(withExtension = true)}#${refName}") } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceFileLoader.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceFileLoader.kt index 00c0ced..7467bff 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceFileLoader.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceFileLoader.kt @@ -7,6 +7,10 @@ import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.data.* +/** + * Interface for loading resources from files. + * @param T The type of resource this loader handles. + */ public interface ResourceFileLoader : Identifiable { /** * The dependencies of this loader. These are the loaders that must run before this one. @@ -16,6 +20,15 @@ public interface ResourceFileLoader : Identifiable { */ public val dependencies: List> get() = listOf() + /** + * Loads a resource from the given data and metadata. + * + * @param ref The reference to the resource being loaded. + * @param data The raw data of the resource. + * @param metadata The metadata associated with the resource, if any. + * @param options Options for resource loading. + * @return The result of the resource loading attempt. + */ public fun loadResource( ref: ResourceRef, data: ByteArray, @@ -25,6 +38,10 @@ public interface ResourceFileLoader : Identifiable { public companion object : IdentifierRegistry>("Resource File Loaders") { + /** + * Returns all registered loaders sorted by their dependencies. + * Throws an exception if circular dependencies are detected. + */ public fun getDependencySortedLoaders(): List> { val sorted = mutableListOf>() val visited = mutableSetOf>() @@ -67,12 +84,13 @@ public interface ResourceFileLoader : Identifiable { */ public sealed class ResourceLoadResult { /** - * The resource has loaded successfully and can be registered! Hooray! + * The resource has loaded successfully and can be registered. Hooray! */ public data class Success(val resource: T) : ResourceLoadResult() /** * The resource failed to load, and we shouldn't try to make it into any other resource type. + * @param exception The exception that caused the failure, if any. */ public class Failure(public val exception: Throwable? = null) : ResourceLoadResult() { override fun toString(): String = "Failure" @@ -87,8 +105,22 @@ public sealed class ResourceLoadResult { } +/** + * Exception thrown when a resource is of the wrong type. + */ private class WrongResourceTypeException : Exception() +/** + * Context for resource file loading, containing all relevant data and helpers. + * + * @param T The resource type. + * @param M The metadata type. + * @property ref The reference to the resource. + * @property data The raw data of the resource. + * @property metadata The parsed metadata, if any. + * @property metadataBytes The raw metadata bytes, if any. + * @property options The resource loading options. + */ public class ResourceFileLoaderContext( public val ref: ResourceRef, public val data: ByteArray, @@ -96,19 +128,41 @@ public class ResourceFileLoaderContext( public val metadataBytes: ByteArray? = null, public val options: ResourceLoadOptions = ResourceLoadOptions() ) { + /** + * The resource data as a UTF-8 string. + */ public val dataAsString: String by lazy { data.toString(Charsets.UTF_8) } + /** + * Returns a successful resource load result. + * @param value The loaded resource. + */ public fun success(value: T): ResourceLoadResult.Success = ResourceLoadResult.Success(value) + + /** + * Returns a failed resource load result. + * @param exception The exception that caused the failure, if any. + */ public fun failure(exception: Throwable? = null): ResourceLoadResult.Failure = ResourceLoadResult.Failure(exception) + /** + * Returns a result indicating the resource was of the wrong type. + */ public fun wrongType(): ResourceLoadResult.WrongType = ResourceLoadResult.WrongType() } /** - * Create a resource loader that only processes resoruces with certain extensions. + * Create a resource loader that only processes resources with certain extensions. * Don't write the extensions with a dot at the beginning. If a resource has multiple - * extensions (such as tar.gz), write it with a period only in the middle + * extensions (such as tar.gz), write it with a period only in the middle. + * + * @param extensions The file extensions this loader should handle. + * @param resourceTypeId The identifier for the resource type. + * @param metadataSerializer The serializer for the resource metadata, if any. + * @param dependencies The dependencies of this loader. + * @param func The function to process the resource loading. + * @return A new ResourceFileLoader instance. */ public fun resourceLoader( extensions: Collection?, @@ -174,6 +228,11 @@ public fun resourceLoader( /** + * Checks if the resource type in the metadata matches the expected type or is unknown. + * + * @param ref The resource reference. + * @param metadataText The metadata as a string. + * @param resourceTypeId The expected resource type identifier. * @return true if it's the resource type or the resource type is unknown, false otherwise. */ internal fun checkIsResourceTypeOrUnknown( @@ -188,6 +247,11 @@ internal fun checkIsResourceTypeOrUnknown( return id(metadataId.toString()) == resourceTypeId } +/** + * Checks if strict resource loading is enabled for the plugin and disables the plugin if so. + * + * @param plugin The plugin to check and possibly disable. + */ internal fun checkResourceLoading(plugin: CuTPlugin) { if (CuTAPI.getDescriptor(plugin).options.strictResourceLoading) { Plugin.error("Strict resource loading is enabled for ${plugin.namespace}. Disabling ${plugin.namespace}...") diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceGenerator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceGenerator.kt index ad56e3d..f54a8b3 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceGenerator.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceGenerator.kt @@ -6,16 +6,39 @@ import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.data.* +/** + * Represents the stages of resource generation. + * These stages determine when a resource generator is executed during the resource lifecycle. + */ public enum class ResourceGenerationStage { + /** + * Executed before resource processors are run. + */ BeforeProcessors, + + /** + * Executed after resource processors are run. + */ AfterProcessors, + + /** + * Executed before resource pack processors are run. + */ BeforePackProcessors, + + /** + * Executed after resource pack processors are run. + */ AfterPackProcessors } /** - * This is called after the server has finished initialization, but before CuTAPI has run the ResourceProcessors. - * This should be used to + * Abstract base class for resource generators. + * Resource generators are responsible for generating resources during specific stages + * of the resource lifecycle. + * + * @property id The unique identifier for the resource generator. + * @property stage The stage at which the generator is executed. */ public abstract class ResourceGenerator( override val id: Identifier, @@ -23,29 +46,53 @@ public abstract class ResourceGenerator( ) : Identifiable { /** - * [context.resource] and resources generated through [ResourceGeneratorContext.register] can NOT have the same [ResourceRef]! - * Use subId to differentiate between them using [ResourceRef.subId] + * Generates or modifies resources during the specified stage. + * + * **Note:** [context.resource] and resources generated through [ResourceGeneratorContext.register] + * must not have the same [ResourceRef]. Use `subId` to differentiate between them using [ResourceRef.subId]. + * + * @param context The context for the resource generation, containing the resource and helper methods. */ public abstract fun generate(context: ResourceGeneratorContext) + /** + * Companion object for managing and retrieving registered resource generators. + */ public companion object : IdentifierRegistry("Resource Generators") { + + /** + * Retrieves all resource generators for a specific stage. + * + * @param stage The stage to filter resource generators by. + * @return A list of resource generators for the specified stage. + */ public fun getByStage(stage: ResourceGenerationStage): List = getAllValues().filter { it.stage == stage } } } +/** + * Context provided to resource generators during execution. + * + * @param T The type of resource being generated or modified. + * @property resource The resource being processed by the generator. + * @property generateBlock The block of metadata associated with the resource. + * @property ref The reference to the resource being processed. + * @property register A function to register newly generated resources. + */ public data class ResourceGeneratorContext( val resource: T, val generateBlock: GenerateBlock, - val suppliedSubId: String, + val ref: ResourceRef, val register: (Resource) -> Unit ) { /** - * Deserialize the options for this generator into a new object. + * Deserializes the options for this generator into a new object. * - * @param S The type you're deserializing into. - * @param serializer The serializer for [S]. + * @param S The type to deserialize into. + * @param serializer The serializer for the type [S]. + * @return The deserialized options object. */ public fun castOptions(serializer: KSerializer): S { return CuTAPI.toml.decodeFromTomlElement(serializer, generateBlock.options) @@ -53,7 +100,12 @@ public data class ResourceGeneratorContext( } /** - * Ran for ALL resources that exist. + * Creates a resource generator that is executed for all resources. + * + * @param id The unique identifier for the generator. + * @param priority The stage at which the generator is executed. + * @param block The logic to execute for each resource. + * @return A [ResourceGenerator] instance. */ public fun resourceGenerator( id: Identifier, @@ -68,7 +120,13 @@ public fun resourceGenerator( } /** - * Ran only for resources of a certain type that has [id] in its `generate` metadata section. + * Creates a resource generator that is executed only for resources of a specific type. + * + * @param T The type of resource to process. + * @param id The unique identifier for the generator. + * @param priority The stage at which the generator is executed. + * @param block The logic to execute for each resource of type [T]. + * @return A [ResourceGenerator] instance. */ @JvmName("resourceGeneratorWithType") public inline fun resourceGenerator( @@ -79,7 +137,7 @@ public inline fun resourceGenerator( return object : ResourceGenerator(id, priority) { override fun generate(context: ResourceGeneratorContext) { if (context.resource is T) { - // very nasty + // Cast the context to the specific type and execute the block. @Suppress("UNCHECKED_CAST") block(context as ResourceGeneratorContext) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceInspector.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceInspector.kt index 417a443..cc6dfe3 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceInspector.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceInspector.kt @@ -4,54 +4,137 @@ import net.kyori.adventure.extra.kotlin.* import net.kyori.adventure.text.* import xyz.mastriel.cutapi.utils.* +/** + * A utility class for inspecting resources by adding and managing inspections. + * Inspections can be of various types, such as single values, lists, maps, or raw components. + */ public class ResourceInspector { + /** + * A mutable list of inspections added to the inspector. + */ private val _inspections = mutableListOf() + /** + * A read-only list of all inspections added to the inspector. + */ public val inspections: List get() = _inspections + /** + * Adds a new inspection to the inspector. + * + * @param inspection The inspection to add. + */ public fun addInspection(inspection: Inspection) { _inspections.add(inspection) } + /** + * Adds a single-value inspection. + * + * @param name The name of the inspection. + * @param value A lambda that provides the value to inspect. + */ public fun single(name: String, value: () -> Any) { addInspection(Inspection.Single(name, value)) } + /** + * Adds a list inspection. + * + * @param name The name of the inspection. + * @param values A lambda that provides the collection of values to inspect. + */ public fun list(name: String, values: () -> Collection) { addInspection(Inspection.List(name, values)) } + /** + * Adds a map inspection. + * + * @param name The name of the inspection. + * @param values A lambda that provides the map of key-value pairs to inspect. + */ public fun map(name: String, values: () -> Map) { addInspection(Inspection.Map(name, values)) } - public fun raw(name: String, component: Component) { + /** + * Adds a raw component inspection. + * + * @param name The name of the inspection. + * @param component The raw component to inspect. + */ + public fun raw(name: String, component: () -> Component) { addInspection(Inspection.Raw(name, component)) } + /** + * Companion object containing default colors for inspection components. + */ public companion object { + /** + * The color used for the inspector title. + */ public val InspectorTitle: Color = Color.of(0x77ffb8) + + /** + * The color used for property keys in inspections. + */ public val PropertyKey: Color = Color.of(0xc4ffb2) + + /** + * The color used for property values in inspections. + */ public val PropertyValue: Color = Color.of(0xf5ff97) } } +/** + * Appends an inspection to a text component builder. + * + * @param text The text of the inspection. + * @param value The value of the inspection (optional for values that require multiple lines, like maps or lists). + * @param spaces The number of spaces to prepend to the text (default is 0). + */ private fun TextComponent.Builder.appendInspection(text: String, value: Any = "", spaces: Int = 0) { appendLine((" ".repeat(spaces) + "&${ResourceInspector.PropertyKey}$text &8→ &${ResourceInspector.PropertyValue}$value").colored) } - +/** + * Represents an inspection that can be added to the [ResourceInspector]. + * Inspections can be of various types, such as single values, lists, maps, or raw components. + */ public sealed interface Inspection { + + /** + * The name of the inspection. + */ public abstract val name: String + /** + * Generates a component representation of the inspection. + * + * @return The component representing the inspection. + */ public fun getInspectionComponent(): Component + /** + * Represents a single-value inspection. + * + * @property name The name of the inspection. + * @property value A lambda that provides the value to inspect. + */ public data class Single( override val name: String, val value: () -> Any ) : Inspection { + /** + * Generates a component representation of the single-value inspection. + * + * @return The component representing the inspection. + */ override fun getInspectionComponent(): Component { return text { appendInspection(name, value()) @@ -59,11 +142,22 @@ public sealed interface Inspection { } } + /** + * Represents a list inspection. + * + * @property name The name of the inspection. + * @property values A lambda that provides the collection of values to inspect. + */ public data class List( override val name: String, val values: () -> Collection ) : Inspection { + /** + * Generates a component representation of the list inspection. + * + * @return The component representing the inspection. + */ override fun getInspectionComponent(): Component { return text { if (values().isEmpty()) { @@ -79,11 +173,22 @@ public sealed interface Inspection { } } + /** + * Represents a map inspection. + * + * @property name The name of the inspection. + * @property values A lambda that provides the map of key-value pairs to inspect. + */ public data class Map( override val name: String, val values: () -> kotlin.collections.Map ) : Inspection { + /** + * Generates a component representation of the map inspection. + * + * @return The component representing the inspection. + */ override fun getInspectionComponent(): Component { return text { if (values().isEmpty()) { @@ -99,12 +204,24 @@ public sealed interface Inspection { } } + /** + * Represents a raw component inspection. + * + * @property name The name of the inspection. + * @property component The raw component to inspect. + */ public data class Raw( override val name: String, - val component: Component + val component: () -> Component ) : Inspection { + + /** + * Generates a component representation of the raw inspection. + * + * @return The component representing the inspection. + */ override fun getInspectionComponent(): Component { - return component + return component() } } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceManager.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceManager.kt index b9c7b77..57e72ff 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceManager.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceManager.kt @@ -9,9 +9,6 @@ import xyz.mastriel.cutapi.resources.data.* import xyz.mastriel.cutapi.resources.process.* import xyz.mastriel.cutapi.utils.* import java.io.* -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set public class ResourceManager { @@ -21,6 +18,14 @@ public class ResourceManager { private val resourceRoots = mutableListOf() + /** + * Registers a resource in the resource manager. + * + * @param resource The resource to register. + * @param overwrite Whether to overwrite an existing resource with the same reference. + * @param log Whether to log the registration process. + * @throws IllegalStateException if the resource is already registered and overwrite is false. + */ public fun register(resource: Resource, overwrite: Boolean = false, log: Boolean = true) { if (resource.ref in resources.keys && !overwrite) { error("Resource '${resource.ref}' already registered.") @@ -34,6 +39,11 @@ public class ResourceManager { resource.onRegister() } + /** + * Registers all parent folders of a given resource reference. + * + * @param ref The resource reference whose parent folders will be registered. + */ private fun registerFolders(ref: ResourceRef<*>) { val folders = mutableListOf() var currentRef: Locator = ref @@ -46,20 +56,45 @@ public class ResourceManager { this.folders.addAll(folders) } - + /** + * Retrieves a resource by its reference. + * + * @param ref The reference of the resource to retrieve. + * @return The resource associated with the reference. + * @throws IllegalStateException if the resource is not found. + */ public fun getResource(ref: ResourceRef): T { return getResourceOrNull(ref) ?: error("$ref is not a valid resource.") } + /** + * Retrieves a resource by its reference or returns null if not found. + * + * @param ref The reference of the resource to retrieve. + * @return The resource associated with the reference, or null if not found. + */ @Suppress("UNCHECKED_CAST") public fun getResourceOrNull(ref: ResourceRef): T? { return resources[ref] as? T } + /** + * Checks if a resource is available and loaded. + * + * @param ref The reference of the resource to check. + * @return True if the resource is available, false otherwise. + */ public fun isAvailable(ref: ResourceRef<*>): Boolean { return resources[ref] != null } + /** + * Retrieves the contents of a folder. + * + * @param root The root of the folder. + * @param folderRef The reference to the folder. + * @return A list of locators representing the folder's contents. + */ public fun getFolderContents(root: ResourceRoot, folderRef: FolderRef): List { if (folderRef.isRoot) { return locators.filter { it.root == root } @@ -69,6 +104,11 @@ public class ResourceManager { .filter { it.parent == folderRef } } + /** + * Retrieves all resources currently registered in the resource manager. + * + * @return A list of all registered resources. + */ public fun getAllResources(): List { return resources.values.toList() } @@ -76,11 +116,19 @@ public class ResourceManager { private val tempFolder = Plugin.dataFolder.appendPath("resources-tmp/").absoluteFile + /** + * Clears the temporary folder used for resource operations. + */ internal fun clearTemp() { tempFolder.deleteRecursively() tempFolder.mkdir() } + /** + * Dumps plugin resources to a temporary folder. + * + * @param plugin The plugin whose resources will be dumped. + */ internal fun dumpPluginResourcesToTemp(plugin: CuTPlugin) { val descriptor = CuTAPI.getDescriptor(plugin) val options = descriptor.options @@ -103,33 +151,48 @@ public class ResourceManager { } /** - * Create a new resource root for your plugin. Resources are loaded from here - * whenever the resource pack is generated. + * Registers a new resource root for a plugin. + * + * @param resourceRoot The resource root to register. */ public fun registerResourceRoot(resourceRoot: ResourceRoot) { resourceRoots += resourceRoot } + /** + * Retrieves a resource root by its namespace. + * + * @param root The namespace of the resource root. + * @return The resource root, or null if not found. + */ public fun getResourceRoot(root: String): ResourceRoot? { return resourceRoots.find { it.namespace == root } } /** - * Removes a resource root from your plugin. + * Unregisters a resource root by its namespace. * - * @return true if a resource root was unregistered, false otherwise. + * @param root The namespace of the resource root. + * @return True if the resource root was unregistered, false otherwise. */ public fun unregisterResourceRoot(root: ResourceRoot): Boolean { return resourceRoots.removeIf { it == root } } /** - * Remove all resource roots from your plugin. + * Unregisters all resource roots associated with a plugin. + * + * @param plugin The plugin whose resource roots will be unregistered. */ public fun unregisterAllResourceRoots(plugin: CuTPlugin) { resourceRoots.removeIf { it.cutPlugin == plugin } } + /** + * Loads all resources from a resource root. + * + * @param root The resource root to load resources from. + */ internal fun loadRootResources(root: ResourceRoot) { val namespace = root.namespace @@ -152,7 +215,7 @@ public class ResourceManager { } /** - * Converts a [FolderRef] to a filesystem file, given a root to be based off of. + * Converts a [FolderRef] to a filesystem file handle, given a root to be based off of. * * @param root The root folder that this is loading from. * @param folderRef The [FolderRef] of this folder. @@ -180,15 +243,11 @@ public class ResourceManager { } /** - * Loads all resources from a folder [root] recursively. + * Recursively finds resources in a folder and adds them to a list. * - * @param root The root folder this is loading from. Used for relative pathing - * @param folder The current [FolderRef]. - * - * Example: - * Loading from /CuTAPI/custom, where custom is the root, and contains a folder - * named 'abc' which itself has a file `texture.png`, [folder] could be 'cutapi://abc', and that - * would load the `texture.png` file into 'cutapi://abc/texture.png' + * @param root The root folder to search in. + * @param folder The current folder reference. + * @param found The list to add found resources to. */ private fun findResourcesInFolder(root: File, folder: FolderRef, found: MutableList>>) { folderRefToFile(root, folder).listFiles()?.toList() @@ -207,10 +266,13 @@ public class ResourceManager { } /** - * Load a resource from a file into a ResourceRef, and registers it. + * Loads a resource from a file and registers it. * - * @param resourceFile The file being loaded - * @param ref The resource ref this file will be associated with + * @param resourceFile The file to load the resource from. + * @param ref The reference of the resource. + * @param withLoader The loader to use for loading the resource. + * @param options Additional options for loading the resource. + * @return The result of the resource loading process. */ public fun loadResource( resourceFile: File, @@ -251,7 +313,11 @@ public class ResourceManager { /** * Loads a resource from a file without registering it. * - * A null return value means that the resource was a metadata file. + * @param resourceFile The file to load the resource from. + * @param ref The reference of the resource. + * @param loader The loader to use for loading the resource. + * @param options Additional options for loading the resource. + * @return The result of the resource loading process. */ @OptIn(InternalSerializationApi::class) public fun loadResourceWithoutRegistering( @@ -281,6 +347,13 @@ public class ResourceManager { } } + /** + * Loads metadata for a resource from a file. + * + * @param metadataFile The file containing the metadata. + * @param ref The reference of the resource. + * @return The metadata as a byte array, or null if not found. + */ private fun loadMetadata(metadataFile: File, ref: ResourceRef<*>): ByteArray? { // TODO we're deserializing this a lot. we should cache it or something. return try { @@ -294,7 +367,8 @@ public class ResourceManager { else TomlTable() - val newTable = folderTable.combine(metadataTable, false) + val newTable = folderTable.combine(metadataTable, true) + println("${ref}: " + CuTAPI.toml.encodeToString(newTable)) CuTAPI.toml.encodeToString(newTable).toByteArray(Charsets.UTF_8) } } catch (e: Exception) { @@ -306,24 +380,37 @@ public class ResourceManager { var depth = 0 var table = CuTAPI.toml.parseToTomlTable(bytes.toString(Charsets.UTF_8)) while (metadataNeedsToProcessExtensions(table)) { + println("Processing extends for $ref at depth $depth ts") depth++ if (depth > depthLimit) { Plugin.error("Metadata extensions for $ref excessively (or infinitely) recurse.") break } - table = processTemplates(table) + table = processTemplates(ref, table) } CuTAPI.toml.encodeToString(table).toByteArray(Charsets.UTF_8) } } + /** + * Checks if a metadata table needs to process extend blocks. + * + * @param table The metadata table to check. + * @return True if extensions need to be processed, false otherwise. + */ private fun metadataNeedsToProcessExtensions(table: TomlTable): Boolean { return table["extends"]?.asTomlTable() != null } - - private fun processTemplates(table: TomlTable): TomlTable { + /** + * Processes templates in a metadata table. + * + * @param ref The reference of the resource. + * @param table The metadata 'extends' table to process. + * @return The processed metadata table. + */ + private fun processTemplates(ref: ResourceRef<*>, table: TomlTable): TomlTable { var metadata = table val extensions = metadata["extends"]?.asTomlTable() ?: return metadata @@ -339,8 +426,10 @@ public class ResourceManager { val newMetadata = refs.fold(metadata) { acc, r -> - val templateTable = r.first.getResource() ?: return@fold acc - val patchedTemplates = r.second.map { templateTable.getPatchedTable(it) } + val templateTable = r.first.getResource() ?: return@fold acc.also { + Plugin.error("Template ${r.first} not found for resource $ref.") + } + val patchedTemplates = r.second.map { templateTable.getPatchedTable(ref, it) } patchedTemplates.fold(acc) { acc2, t -> acc2.combine(t, true) @@ -351,7 +440,14 @@ public class ResourceManager { } /** - * Loads a resource from just a ref, resourceBytes, and metadataBytes. + * Attempts to load a resource from its reference, resource bytes, and metadata bytes. + * + * @param ref The reference of the resource. + * @param resourceBytes The resource data as a byte array. + * @param metadataBytes The metadata as a byte array. + * @param loader The loader to use for loading the resource. + * @param options Additional options for loading the resource. + * @return The result of the resource loading process. */ @Suppress("UNCHECKED_CAST") private fun tryLoadResource( @@ -378,6 +474,14 @@ public class ResourceManager { } } + /** + * Creates clones of a resource based on its metadata. + * + * @param resource The resource to clone. + * @param resourceBytes The resource data as a byte array. + * @param metadataBytes The metadata as a byte array. + * @param loader The loader to use for loading the clones. + */ @Suppress("UNCHECKED_CAST") private fun createClones( resource: Resource, @@ -386,58 +490,68 @@ public class ResourceManager { loader: ResourceFileLoader<*> ) { if (metadataBytes == null) return - val originMetadataTable = CuTAPI.toml.parseToTomlTable(metadataBytes.toString(Charsets.UTF_8)) - val cloneBlocks = try { - originMetadataTable.getArrayOrNull("clone") ?: listOf() - } catch (ex: Exception) { - ex.printStackTrace() - listOf() - } + val originMetadataTable = CuTAPI.toml.parseToTomlTable(metadataBytes.toString(Charsets.UTF_8)) + val cloneBlocks = extractCloneBlocks(originMetadataTable) - for (cloneBlock in cloneBlocks.mapNotNull { it as? TomlTable }) { + for (cloneBlock in cloneBlocks) { try { - val originMetadataMap = originMetadataTable.toMutableMap() - // we don't want infinite cloning - originMetadataMap.remove("clone") - - val patchedMetadata = TomlTable(originMetadataMap) - - val newTable = patchedMetadata.combine(cloneBlock, false) - - val newRefSubId = newTable.getStringOrNull("clone_sub_id") - ?: throw SerializationException("Clone block must have a 'clone_sub_id' field.") - - val newMetadataBytes = CuTAPI.toml.encodeToString(newTable).toByteArray(Charsets.UTF_8) - val newRef = resource.ref.cloneSubId(newRefSubId) - - when (val result = - tryLoadResource(newRef, resourceBytes, newMetadataBytes, loader as ResourceFileLoader)) { - is ResourceLoadResult.Success -> { - register(result.resource, overwrite = true) - } - - is ResourceLoadResult.Failure -> { - Plugin.error("Failed to clone resource ${resource.ref}. (wrong type)") - checkResourceLoading(resource.plugin) - } - - is ResourceLoadResult.WrongType -> { - Plugin.error("Failed to clone resource ${resource.ref}. (wrong type)") - checkResourceLoading(resource.plugin) - } - - else -> {} - } + val newMetadataBytes = generateCloneMetadata(originMetadataTable, cloneBlock) + val newRef = createCloneReference(resource, newMetadataBytes) + loadAndRegisterClone(newRef, resourceBytes, newMetadataBytes, loader) } catch (ex: Exception) { Plugin.error("Failed to clone resource ${resource.ref}.") ex.printStackTrace() } + } + } + private fun extractCloneBlocks(originMetadataTable: TomlTable): List { + return try { + originMetadataTable.getArrayOrNull("clone")?.filterIsInstance() ?: emptyList() + } catch (ex: Exception) { + ex.printStackTrace() + emptyList() } } + private fun generateCloneMetadata(originMetadataTable: TomlTable, cloneBlock: TomlTable): ByteArray { + val patchedMetadata = TomlTable(originMetadataTable.filterKeys { it != "clone" }) + val combinedMetadata = patchedMetadata.combine(cloneBlock, false) + return CuTAPI.toml.encodeToString(combinedMetadata).toByteArray(Charsets.UTF_8) + } + + private fun createCloneReference(resource: Resource, newMetadataBytes: ByteArray): ResourceRef<*> { + val newTable = CuTAPI.toml.parseToTomlTable(newMetadataBytes.toString(Charsets.UTF_8)) + val newSubId = newTable.getStringOrNull("clone_sub_id") + ?: throw SerializationException("Clone block must have a 'clone_sub_id' field.") + return resource.ref.cloneSubId(newSubId) + } + + @Suppress("UNCHECKED_CAST") + private fun loadAndRegisterClone( + newRef: ResourceRef<*>, + resourceBytes: ByteArray, + newMetadataBytes: ByteArray, + loader: ResourceFileLoader<*> + ) { + when (val result = + tryLoadResource(newRef, resourceBytes, newMetadataBytes, loader as ResourceFileLoader)) { + is ResourceLoadResult.Success -> register(result.resource, overwrite = true) + is ResourceLoadResult.Failure -> Plugin.error("Failed to clone resource $newRef. (loading failure)") + is ResourceLoadResult.WrongType -> Plugin.error("Failed to clone resource $newRef. (wrong type)") + } + } + + /** + * Writes a resource and its metadata to a temporary folder if needed. + * + * @param plugin The plugin associated with the resource. + * @param ref The reference of the resource. + * @param resourceFile The file containing the resource data. + * @param metadataFile The file containing the metadata. + */ public fun writeToResourceTmpIfNeeded( plugin: CuTPlugin, ref: ResourceRef<*>, @@ -458,6 +572,12 @@ public class ResourceManager { } + /** + * Retrieves the default metadata table for a folder. + * + * @param ref The reference of the folder. + * @return The default metadata table, or null if not found. + */ private fun getFolderDefaultTable(ref: ResourceRef<*>): TomlTable? { val parent = ref.parent ?: return null @@ -467,7 +587,9 @@ public class ResourceManager { /** - * Runs [Resource.check] on all resources. + * Runs checks on all registered resources. + * + * @throws ResourceCheckException if a resource fails its check. */ internal fun checkAllResources() { resources.forEach { (ref, res) -> diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourcePackManager.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourcePackManager.kt index 8e04cf9..4c0631d 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourcePackManager.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourcePackManager.kt @@ -15,66 +15,89 @@ import kotlin.time.* public class ResourcePackManager { + /** + * The temporary folder used for resource pack generation. + */ public val tempFolder: File = Plugin.dataFolder.appendPath("pack-tmp/") + /** + * The name of the generated resource pack zip file. + */ private val zipName by cutConfigValue("generated-pack-name", "pack.zip") + + /** + * The file where the generated resource pack zip is stored. + */ public val zipFile: File = Plugin.dataFolder.appendPath(zipName) + /** + * Information about the generated resource pack, including its URL and hash. + */ public var packInfo: PackInfo? = null private set /** - * Gets the textures folder of a plugin in the resource pack. + * Gets the textures folder for a specific namespace in the resource pack. * * @param namespace The namespace of the plugin. + * @return The folder where textures are stored. */ public fun getTexturesFolder(namespace: String): File { return File(tempFolder, "assets/$namespace/textures/item") } /** - * Gets the textures folder of a plugin in the resource pack. + * Gets the models folder for a specific namespace in the resource pack. * * @param namespace The namespace of the plugin. + * @return The folder where models are stored. */ public fun getModelFolder(namespace: String): File { return File(tempFolder, "assets/$namespace/models/") } /** - * Gets the item model folder of a plugin in the resource pack. + * Gets the item model folder for a specific namespace in the resource pack. * * @param namespace The namespace of the plugin. + * @return The folder where item models are stored. */ public fun getItemModelFolder(namespace: String): File { - return File(tempFolder, "assets/$namespace/models/item/") + return File(tempFolder, "assets/$namespace/items/") } /** - * Gets the textures folder of a plugin in the resource pack. + * Gets the textures folder for a specific plugin in the resource pack. * * @param plugin The plugin. Will fail if this plugin is not registered in CuTAPI. + * @return The folder where textures are stored. */ public fun getTexturesFolder(plugin: CuTPlugin): File { val namespace = CuTAPI.getDescriptor(plugin).namespace return getTexturesFolder(namespace).also { it.mkdirs() } } - + /** + * The generator used to create the resource pack. + */ public val generator: ResourcePackGenerator get() { - // TODO("detect versions dynamically") + // TODO: Detect versions dynamically return PackVersion46Generator() } /** - * Checks if the zip file exists and is ready to be uploaded + * Checks if the zip file exists and is ready to be uploaded. + * + * @return `true` if the zip file exists, `false` otherwise. */ public fun zipReady(): Boolean = zipFile.exists() - /** - * Zip the resource pack and upload it. + * Zips the resource pack and uploads it using the active uploader. + * + * @return The generated [PackInfo] containing the URL and hash of the resource pack. + * @throws PackGenerationException If zipping or uploading fails. */ internal suspend fun zipAndUpload(): PackInfo { try { @@ -82,6 +105,7 @@ public class ResourcePackManager { } catch (e: Exception) { throw PackGenerationException("Failed to zip the resource pack.", e) } + var activeUploader = Uploader.getActive() if (activeUploader == null) { Plugin.warn("Could not find uploader with id '${Uploader.uploaderId}'. Using 'cutapi:internal' instead.") @@ -100,13 +124,21 @@ public class ResourcePackManager { return PackInfo(url, packHash).also { packInfo = it } } - + /** + * Zips the resource pack folder into a single zip file. + */ private fun zipPack() { if (zipFile.exists()) zipFile.delete() zipFolder(tempFolder, zipFile) } - + /** + * Regenerates the resource pack by clearing temporary resources, loading resources, + * running processors, and zipping the pack. + * + * @return The generated [PackInfo]. + * @throws PackGenerationException If resource loading or generation fails. + */ public suspend fun regenerate(): PackInfo = withContext(Plugin.minecraftDispatcher) { try { CuTAPI.resourceManager.clearTemp() @@ -133,9 +165,17 @@ public class ResourcePackManager { } } + /** + * Event triggered when the resource pack generation is finished. + */ public val onPackGenerateFinished: EventHandlerList = EventHandlerList() - + /** + * Sanitizes a resource name by replacing special characters with safe alternatives. + * + * @param name The resource name to sanitize. + * @return The sanitized name. + */ public fun sanitizeName(name: String): String { return name.replace(Locator.SUBRESOURCE_SEPARATOR, "__sre__") .replace(Locator.GENERATED_SEPARATOR, "__gen__") @@ -143,14 +183,16 @@ public class ResourcePackManager { .lowercase() } + /** + * Runs all registered resource processors on the resources in the [ResourceManager]. + */ private fun runResourceProcessors() { val executionTime = measureTime { generateResources(CuTAPI.resourceManager.getAllResources(), ResourceGenerationStage.BeforeProcessors) - // we always want the generate processor to run last. + // Ensure the generate processor runs last. ResourceProcessor.forEach { it.processResources(CuTAPI.resourceManager) } - generateResources(CuTAPI.resourceManager.getAllResources(), ResourceGenerationStage.AfterProcessors) } Plugin.info("Resource Processors (normal registry) ran in $executionTime.") diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceProcessor.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceProcessor.kt index 5e1b6a8..7d83032 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceProcessor.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceProcessor.kt @@ -2,44 +2,67 @@ package xyz.mastriel.cutapi.resources import xyz.mastriel.cutapi.registry.* - /** - * A resource processor will be executed when the server has finished loading, and all resources - * are registered. You should not modify the [ResourceManager], as this period is closed. - * If you add more resources anyway, they will not go through the processor. + * Represents a processor that is executed after the server has finished loading and all resources + * have been registered. This processor is used to perform operations on resources without modifying + * the [ResourceManager]. * - * If you want to add more resources using generators, see [ResourceGenerator]. Generated resources - * are also processed here. + * **Important:** Do not modify the [ResourceManager] during this phase. Any resources added will not + * go through the processor. * - * If you want to prepare your resources to be processed into a resource pack, register to [ResourcePackProcessor] - * instead. Both registries use the same type of [ResourceProcessor]. + * - To add resources using generators, see [ResourceGenerator]. + * - To prepare resources for a resource pack, use [ResourcePackProcessor]. * * @see [resourceProcessor] * @see [ResourcePackProcessor] */ public fun interface ResourceProcessor { + + /** + * Processes the resources managed by the [ResourceManager]. + * + * @param resources The [ResourceManager] containing all registered resources. + */ public fun processResources(resources: ResourceManager) + /** + * A registry for managing all registered [ResourceProcessor] instances. + * + * @property name The name of the registry, which is "Resource Processors". + */ public companion object : ListRegistry("Resource Processors") } - /** - * A resource pack processor will be executed while the server is trying to generate a resource pack. - * You should not modify the [ResourceManager], or make any unnecessary modifiacations to [Resource]s you're - * processing. + * Represents a processor that is executed while the server is generating a resource pack. + * This processor is used to process resources into a resource pack without modifying the + * [ResourceManager] or making unnecessary modifications to the [Resource]s being processed. * - * If you want to add more resources using generators, see [ResourceGenerator]. + * - To add resources using generators, see [ResourceGenerator]. * * @see [resourceProcessor] * @see [ResourceProcessor] */ public object ResourcePackProcessor : ListRegistry("Resource Pack Processors") - +/** + * Provides a context for processing a specific type of resource. + * + * @param T The type of resource being processed. + * @property resources The list of resources of type [T] to be processed. + */ public data class ResourceProcessorContext(val resources: List) - +/** + * Creates a [ResourceProcessor] for a specific type of resource. + * + * This function filters resources of type [T] from the [ResourceManager] and provides them + * to the given processing block. + * + * @param T The type of resource to process. + * @param block The processing logic to apply to the filtered resources. + * @return A [ResourceProcessor] that processes resources of type [T]. + */ public inline fun resourceProcessor(crossinline block: ResourceProcessorContext.() -> Unit): ResourceProcessor { return ResourceProcessor { resources -> val filteredResources = resources.getAllResources().filterIsInstance() diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRef.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRef.kt index 48f08cf..340b4dc 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRef.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRef.kt @@ -13,7 +13,7 @@ import kotlin.reflect.* /** * A reference to a resource that may or may not exist. - * Use getResource to get the actual resource this is referring to (if it exists) + * Use `getResource` to retrieve the actual resource this refers to (if it exists). */ @Serializable(with = ResourceRefSerializer::class) @ConsistentCopyVisibility @@ -22,45 +22,75 @@ public data class ResourceRef internal constructor( override val pathList: List ) : ReadOnlyProperty, Locator { - /** - * This doesn't actually ever hold anything of the type T, so this is a safe cast - * You might want to make sure that the resource is actually of type T before using this. + * Casts this `ResourceRef` to a different type. + * @return A `ResourceRef` of the specified type. */ @Suppress("UNCHECKED_CAST") public fun cast(): ResourceRef = this as ResourceRef + /** + * Retrieves the resource this reference points to, or null if it doesn't exist. + * @return The resource of type `T`, or null if unavailable. + */ public fun getResource(): T? { return CuTAPI.resourceManager.getResourceOrNull(this) } + /** + * Gets the type of the resource this reference points to. + * @return The `KClass` of the resource type, or null if unavailable. + */ val resourceType: KClass? get(): KClass? { return getResource()?.let { it::class } } + override val path: String get() = pathList.joinToString("/") + + /** + * Retrieves the metadata associated with the resource. + * @return The metadata, or null if unavailable. + */ public fun getMetadata(): CuTMeta? { return getResource()?.metadata } + /** + * Checks if the resource is available in the resource manager. + * @return `true` if the resource is available, `false` otherwise. + */ public fun isAvailable(): Boolean { return CuTAPI.resourceManager.isAvailable(this) } - override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + /** + * Retrieves the resource this reference points to when used as a property delegate. + * @param thisRef The object containing the property. + * @param property The property being accessed. + * @return The resource of type `T`, or null if unavailable. + */ + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T? { return getResource() } - - override val path: String get() = pathList.joinToString("/") - + /** + * Constructs the path of the resource with optional formatting. + * @param withExtension Whether to include the file extension. + * @param withNamespace Whether to include the namespace. + * @param withRootAlias Whether to include the root alias. + * @param withNamespaceAsFolder Whether to include the namespace as a folder. + * @param withName Whether to include the resource name. + * @param fixInvalids Whether to fix invalid characters in the path. + * @return The formatted path as a string. + */ public fun path( withExtension: Boolean = false, withNamespace: Boolean = false, withRootAlias: Boolean = withNamespace, withNamespaceAsFolder: Boolean = false, withName: Boolean = true, - fixInvalids: Boolean = true + fixInvalids: Boolean = false ): String { val sb = StringBuilder("") if (withNamespace) { @@ -87,11 +117,17 @@ public data class ResourceRef internal constructor( return sb.toString().removeSuffix("/").fixInvalids() } + /** + * Gets the name of the resource (last segment of the path). + */ val name: String get() = path .split("/") .last() + /** + * Gets the file extension of the resource. + */ val extension: String get() = name .split(Locator.SUBRESOURCE_SEPARATOR, Locator.GENERATED_SEPARATOR, Locator.CLONE_SEPARATOR, limit = 2) @@ -99,6 +135,9 @@ public data class ResourceRef internal constructor( .split(".", limit = 2) .last() + /** + * Gets the parent folder reference of this resource, or null if it has no parent. + */ override val parent: FolderRef? get() { val list = pathList.dropLast(1) @@ -106,59 +145,104 @@ public data class ResourceRef internal constructor( return folderRef(root, list.joinToString("/")) } + /** + * Converts this resource reference to an identifier. + * @return The `Identifier` representing this resource reference. + */ public fun toIdentifier(): Identifier { return id(plugin, path) } - + /** + * Converts this resource reference to a string representation. + * @return The string representation of the resource reference. + */ override fun toString(): String { return path(withExtension = true, withRootAlias = false, withNamespace = true) } - } +/** + * Converts an `Identifier` to a `ResourceRef`. + * @return The `ResourceRef` corresponding to the identifier. + */ public fun Identifier.toResourceRef(): ResourceRef { if (plugin == null) error("Identifier doesn't have an associated plugin.") return ref(plugin!!, key) } - +/** + * Normalizes a resource path by replacing backslashes with forward slashes + * and removing leading/trailing slashes. + * @param path The path to normalize. + * @return The normalized path. + */ public fun normalizeRefPath(path: String): String { return path.replace("\\", "/").removeSuffix("/").removePrefix("/") } +/** + * Creates a `ResourceRef` from a root and a path. + * @param root The resource root. + * @param path The resource path. + * @return The `ResourceRef` for the specified root and path. + */ public fun ref(root: ResourceRoot, path: String): ResourceRef { return ResourceRef(root, normalizeRefPath(path).split("/").filterNot { it.isEmpty() }) } +/** + * Creates a `ResourceRef` from a folder and a path. + * @param folder The folder reference. + * @param path The resource path relative to the folder. + * @return The `ResourceRef` for the specified folder and path. + */ public fun ref(folder: FolderRef, path: String): ResourceRef { return folder.child(path) } - +/** + * Creates a `ResourceRef` from a string path in the format `namespace://path`. + * @param stringPath The string path. + * @return The `ResourceRef` for the specified string path. + */ public fun ref(stringPath: String): ResourceRef { require("://" in stringPath) { "String ResourceRef $stringPath does not follow namespace://path format." } val (start, path) = stringPath.split("://", limit = 2) val startSplit = start.split(Locator.ROOT_SEPARATOR, limit = 2) val namespace = startSplit[0] - val root = startSplit.getOrNull(1) val plugin = CuTAPI.getPluginFromNamespace(namespace) return ref(plugin, path) } - +/** + * Serializer for `ResourceRef` objects. + */ public object ResourceRefSerializer : KSerializer> { + /** + * The descriptor for the `ResourceRef` serializer. + */ override val descriptor: SerialDescriptor get() = PrimitiveSerialDescriptor(this::class.qualifiedName!!, PrimitiveKind.STRING) + /** + * Deserializes a `ResourceRef` from a string. + * @param decoder The decoder to use. + * @return The deserialized `ResourceRef`. + */ override fun deserialize(decoder: Decoder): ResourceRef<*> { val text = decoder.decodeString() return ref(text) } + /** + * Serializes a `ResourceRef` to a string. + * @param encoder The encoder to use. + * @param value The `ResourceRef` to serialize. + */ override fun serialize(encoder: Encoder, value: ResourceRef<*>) { encoder.encodeString(value.toString()) } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRoot.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRoot.kt index 4b4280e..cd9963c 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRoot.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/ResourceRoot.kt @@ -3,25 +3,56 @@ package xyz.mastriel.cutapi.resources import xyz.mastriel.cutapi.* import java.io.* +/** + * Represents the root of a resource hierarchy. + * A `ResourceRoot` is associated with a plugin and provides access to the folder + * where resources are stored. + */ public interface ResourceRoot { + + /** + * The plugin associated with this resource root. + */ public val cutPlugin: CuTPlugin - public val namespace: String get() = cutPlugin.descriptor.namespace /** - * The folder in the file system where resources are stored. + * The namespace of the resource root, derived from the plugin's descriptor. + */ + public val namespace: String + get() = cutPlugin.descriptor.namespace + + /** + * Retrieves the folder in the file system where resources are stored. + * @return The folder containing the resources. */ public fun getResourcesFolder(): File } +/** + * A file system-based implementation of `ResourceRoot`. + * This implementation associates a resource root with a specific folder on the file system. + * + * @property cutPlugin The plugin associated with this resource root. + * @property alias An alias for the resource root, used to distinguish it from others. + * @property folder The folder in the file system where resources are stored. + */ public data class FSResourceRoot( override val cutPlugin: CuTPlugin, val alias: String, val folder: File ) : ResourceRoot { + /** + * The namespace of the resource root, which combines the plugin's namespace + * with the alias, separated by the root separator. + */ override val namespace: String get() = cutPlugin.descriptor.namespace + Locator.ROOT_SEPARATOR + alias - + + /** + * Retrieves the folder in the file system where resources are stored. + * @return The folder containing the resources. + */ override fun getResourcesFolder(): File { return folder } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/VanillaRef.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/VanillaRef.kt new file mode 100644 index 0000000..16fc7a1 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/VanillaRef.kt @@ -0,0 +1,14 @@ +package xyz.mastriel.cutapi.resources + +import kotlinx.serialization.* + +/** + * Represents a reference to a vanilla resource. + * This is a lightweight wrapper around a string that holds the reference. + * + * @property ref The string representation of the vanilla resource reference. + */ +@JvmInline +@Serializable +public value class VanillaRef(public val ref: String) { +} diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/CustomModelDataAllocated.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/CustomModelDataAllocated.kt index 331ca4d..35b6cef 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/CustomModelDataAllocated.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/CustomModelDataAllocated.kt @@ -1,14 +1,30 @@ package xyz.mastriel.cutapi.resources.builtin +/** + * Deprecated: Use ItemModel instead of CustomModelData. + * + * Provides allocation and interface for custom model data values. + */ @Deprecated("Up for removal due to changing from CustomModelData to ItemModel") internal var customModelDataCounter = 32120 +/** + * Deprecated: Use ItemModel instead of CustomModelData. + * + * Allocates a new custom model data value. + * @return The allocated custom model data integer. + */ @Deprecated("Up for removal due to changing from CustomModelData to ItemModel") public fun allocateCustomModelData(): Int { customModelDataCounter += 1 return customModelDataCounter - 1 } +/** + * Deprecated: Use ItemModel instead of CustomModelData. + * + * Interface for objects that have custom model data. + */ @Deprecated("Up for removal due to changing from CustomModelData to ItemModel") public interface CustomModelDataAllocated { public val customModelData: Int? diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/FolderApplyResource.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/FolderApplyResource.kt index 2b553fb..8ca1ab2 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/FolderApplyResource.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/FolderApplyResource.kt @@ -8,11 +8,19 @@ import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.resources.data.* +/** + * A resource representing a folder-level metadata application. + * + * @constructor Creates a FolderApplyResource with the given reference and metadata. + */ public class FolderApplyResource( ref: ResourceRef, metadata: Metadata ) : MetadataResource(ref, metadata) { + /** + * Metadata for FolderApplyResource, containing TOML table to apply. + */ @Serializable public data class Metadata( @SerialName("apply") @@ -20,7 +28,9 @@ public class FolderApplyResource( ) : CuTMeta() } - +/** + * Loader for FolderApplyResource. + */ public val FolderApplyResourceLoader: ResourceFileLoader> = metadataResourceLoader( listOf("meta.folder"), diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/JsonResource.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/JsonResource.kt index f968116..b03b3b3 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/JsonResource.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/JsonResource.kt @@ -6,6 +6,12 @@ import xyz.mastriel.cutapi.* import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.* +/** + * Represents a resource loaded from a JSON file. + * + * @property ref The reference to this JSON resource. + * @property data The parsed JSON object data. + */ public open class JsonResource( override val ref: ResourceRef, public val data: JsonObject @@ -19,6 +25,9 @@ public open class JsonResource( public operator fun component1(): JsonObject = data } +/** + * Loader for JsonResource, parses JSON files into JsonResource objects. + */ public val JsonResourceLoader: ResourceFileLoader = resourceLoader( extensions = listOf("json"), resourceTypeId = id(Plugin, "json"), diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Model3D.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Model3D.kt index 3306945..b086e48 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Model3D.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Model3D.kt @@ -23,7 +23,6 @@ public open class Model3D( inspector.single("Materials") { materials.joinToString() } inspector.single("Block Strategies") { metadata.blockStrategies.joinToString() } inspector.map("Textures") { metadata.textures.mapValues { (_, v) -> v.toString() } } - } @Serializable @@ -41,10 +40,11 @@ public open class Model3D( override fun getItemModel(): VanillaItemModel { return VanillaItemModel( - "${ref.namespace}:/items/${ + "${ref.namespace}:${ ref.path( withExtension = false, - withNamespaceAsFolder = false + withNamespaceAsFolder = false, + fixInvalids = true ) }" ) @@ -81,8 +81,6 @@ public val Model3DResourceLoader: ResourceFileLoader = resourceLoader( val json = jsonObject.toMutableMap() json["textures"] = JsonObject(textures) - println(JsonObject(json)) - val structure = CuTAPI.json.decodeFromJsonElement(JsonObject(json)) success(Model3D(ref, structure, metadata)) } catch (ex: Exception) { @@ -122,6 +120,8 @@ public data class Model3DDisplay( val scale: VoxelVector? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val rotation: VoxelVector? = null, + @EncodeDefault(EncodeDefault.Mode.NEVER) + val translation: VoxelVector? = null, ) @Serializable diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TemplateMetadata.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TemplateMetadata.kt index 9cb3b7f..27e04be 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TemplateMetadata.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TemplateMetadata.kt @@ -8,6 +8,11 @@ import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.resources.data.* +/** + * A resource representing a template with metadata and string data for patching. + * + * @constructor Creates a TemplateResource with the given reference, metadata, and string data. + */ public class TemplateResource( ref: ResourceRef, metadata: Metadata, @@ -17,16 +22,20 @@ public class TemplateResource( @Serializable public class Metadata : CuTMeta() - public fun getPatchedTable(map: Map): TomlTable { + public fun getPatchedTable(baseRef: ResourceRef<*>, map: Map): TomlTable { var stringTable = stringData for ((key, value) in map) { stringTable = stringTable.replace("{{${key}}}", value.toString()) } + stringTable = stringTable.replace("{{@ref}}", baseRef.toString()) return CuTAPI.toml.parseToTomlTable(stringTable) } } +/** + * Loader for TemplateResource. + */ public val TemplateResourceLoader: ResourceFileLoader = resourceLoader( listOf("template"), @@ -37,6 +46,7 @@ public val TemplateResourceLoader: ResourceFileLoader = } +/** + * Type alias for a serializable reference to a TemplateResource. + */ public typealias SerializableTemplateRef = ResourceRef<@Contextual TemplateResource> - - diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Texture2D.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Texture2D.kt index d7f4eb1..94d559a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Texture2D.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/Texture2D.kt @@ -113,12 +113,12 @@ public open class Texture2D( override fun createItemModelData(): JsonObject { val internalJson = CuTAPI.toml.encodeToTomlElement(metadata.itemModelData).asTomlTable().toJson() var jsonObject: JsonObject = internalJson - if (metadata.modelFile != null && metadata.modelFile!!.isAvailable()) { - val (fileJson) = metadata.modelFile?.getResource()!! + if (metadata.modelFile != null && metadata.modelFile.isAvailable()) { + val (fileJson) = metadata.modelFile.getResource()!! jsonObject = internalJson.combine(fileJson) } - val path = ref.path(withExtension = false, withNamespaceAsFolder = false).fixInvalidResourcePath() + val path = ref.path(withExtension = false, withNamespaceAsFolder = false, fixInvalids = true) val texturesObject = jsonObject["textures"]?.jsonObject val textures = texturesObject?.toMutableMap() ?: mutableMapOf() @@ -134,7 +134,8 @@ public open class Texture2D( "${ref.namespace}:${ ref.path( withExtension = false, - withNamespaceAsFolder = false + withNamespaceAsFolder = false, + fixInvalids = true ) }" ) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TextureLike.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TextureLike.kt index a2598a7..b218dfd 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TextureLike.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/builtin/TextureLike.kt @@ -11,6 +11,8 @@ public interface TextureLike { public fun getItemModel(): VanillaItemModel + public val ref: ResourceRef<*> + public val materials: List public val resource: Resource @@ -20,6 +22,10 @@ public interface TextureLike { @JvmInline public value class VanillaItemModel(public val location: String) { + public fun getLocationWithItemFolder(): String { + return "${toIdentifier().namespace}:item/${toIdentifier().key}" + } + public fun toIdentifier(): Identifier { if (":" !in location) { return id(MinecraftAssets, location) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/data/CuTMeta.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/data/CuTMeta.kt index f357789..d0ae7b7 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/data/CuTMeta.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/data/CuTMeta.kt @@ -10,7 +10,7 @@ import xyz.mastriel.cutapi.utils.* @Serializable public open class CuTMeta { @SerialName("generate") - public open val generateBlock: List = listOf() + public open val generateBlocks: List = listOf() /** * Clone blocks will create a whole new resource, diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/data/minecraft/ItemModelData.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/data/minecraft/ItemModelData.kt index 02a58be..a03942d 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/data/minecraft/ItemModelData.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/data/minecraft/ItemModelData.kt @@ -1,28 +1,33 @@ package xyz.mastriel.cutapi.resources.data.minecraft import kotlinx.serialization.* +import xyz.mastriel.cutapi.resources.* +import xyz.mastriel.cutapi.resources.builtin.* + +/** + * Data class representing item model data for Minecraft resources. + * + * @property parent The parent model string. + * @property textures The map of texture keys to texture paths. + * @property overrides List of item model overrides. + */ @Serializable public data class ItemModelData( - val parent: String = "minecraft:item/generated", - - @OptIn(ExperimentalSerializationApi::class) - @EncodeDefault(EncodeDefault.Mode.NEVER) - val textures: Map = mapOf(), + @SerialName("parent") + public val parent: String = "minecraft:item/generated", @OptIn(ExperimentalSerializationApi::class) @EncodeDefault(EncodeDefault.Mode.NEVER) - val overrides: List = listOf() -) - -@Serializable -public data class ItemOverrides( - val predicate: ItemPredicates, - val model: String -) - -@Serializable -public data class ItemPredicates( - @SerialName("custom_model_data") - val customModelData: Int -) \ No newline at end of file + @SerialName("textures") + private val _textures: Map = mapOf(), +) { + @Transient + public val textures: Map = _textures.toMutableMap().also { + for ((key, value) in it) { + if ("://" in value) { + it[key] = ref(value).toMinecraftLocator() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackGenerationException.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackGenerationException.kt index cc2465c..7619981 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackGenerationException.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackGenerationException.kt @@ -1,4 +1,10 @@ package xyz.mastriel.cutapi.resources.generator +/** + * Exception thrown when resource pack generation fails. + * + * @param reason The reason for the failure. + * @param causedBy The underlying cause of the failure, if any. + */ public class PackGenerationException(reason: String, causedBy: Throwable? = null) : Exception(reason, causedBy) { } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackVersion46Generator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackVersion46Generator.kt index bbbac9a..3474973 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackVersion46Generator.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/PackVersion46Generator.kt @@ -1,5 +1,10 @@ package xyz.mastriel.cutapi.resources.generator +/** + * Resource pack generator for Minecraft pack format version 46. + * + * Implements the generation steps for version 46 resource packs. + */ public class PackVersion46Generator : ResourcePackGenerator() { override val packVersion: Int = 46 override val generationSteps: Int = 2 diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/ResourcePackGenerator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/ResourcePackGenerator.kt index 7a81256..d2ebe25 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/ResourcePackGenerator.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/generator/ResourcePackGenerator.kt @@ -10,6 +10,11 @@ import xyz.mastriel.cutapi.utils.* import java.io.* import kotlin.time.* +/** + * Abstract base class for resource pack generators. + * + * Implementations should define the pack version, number of steps, and the generation logic. + */ public abstract class ResourcePackGenerator { /** diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssetLoader.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssetLoader.kt index 3dfc137..a736e2a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssetLoader.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssetLoader.kt @@ -6,6 +6,11 @@ import xyz.mastriel.cutapi.resources.builtin.* import xyz.mastriel.cutapi.utils.* import java.io.* +/** + * Loader for Minecraft assets from the file system. + * + * Loads textures and models from the specified asset folders and registers them as resources. + */ public class MinecraftAssetLoader { private val resourceManager get() = CuTAPI.resourceManager diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssets.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssets.kt index 92a9f01..6897a66 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssets.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/minecraft/MinecraftAssets.kt @@ -6,6 +6,11 @@ import xyz.mastriel.cutapi.resources.* import xyz.mastriel.cutapi.utils.* import java.io.* +/** + * Singleton object representing the Minecraft assets resource root. + * + * Provides access to the Minecraft textures and models folders as FolderRefs. + */ public data object MinecraftAssets : CuTPlugin, ResourceRoot { override val namespace: String = "minecraft" override val plugin: Plugin = Plugin diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackConfig.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackConfig.kt index 5b951d9..ce8883a 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackConfig.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackConfig.kt @@ -2,6 +2,11 @@ package xyz.mastriel.cutapi.resources.pack import xyz.mastriel.cutapi.utils.* +/** + * Configuration values for resource pack generation. + * + * Provides access to the pack PNG, description, and name from configuration. + */ public object PackConfig { public val PackPng: String by cutConfigValue("pack-png", "pack.png") public val PackDescription: String by cutConfigValue("pack-description", "CuTAPI Generated Resource Pack") diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackInfo.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackInfo.kt index 3c65e5e..a6976dd 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackInfo.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/pack/PackInfo.kt @@ -1,3 +1,9 @@ package xyz.mastriel.cutapi.resources.pack +/** + * Data class containing information about a generated resource pack. + * + * @property packUrl The URL where the pack can be downloaded. + * @property packHash The hash of the pack for validation. + */ public data class PackInfo(val packUrl: String, val packHash: String) \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/GenerateProcessor.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/GenerateProcessor.kt index 0bf4665..36cd50e 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/GenerateProcessor.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/GenerateProcessor.kt @@ -1,16 +1,78 @@ package xyz.mastriel.cutapi.resources.process +import kotlinx.serialization.* +import net.peanuuutz.tomlkt.* import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.registry.* import xyz.mastriel.cutapi.resources.* +import xyz.mastriel.cutapi.resources.builtin.* +import xyz.mastriel.cutapi.resources.data.* +public class GenerateResource( + override val ref: ResourceRef, + override val metadata: Metadata +) : MetadataResource(ref, metadata) { + + @Serializable + public data class Metadata( + @SerialName("gen_id") + val generatorId: Identifier, + @SerialName("base_id") + val baseId: ResourceRef<@Contextual Resource>, + val options: TomlTable + ) : CuTMeta() + + public companion object { + public val Loader: ResourceFileLoader = metadataResourceLoader( + extensions = listOf("gen.toml"), + resourceTypeId = id(Plugin, "generate"), + serializer = Metadata.serializer(), + ) { + success(GenerateResource(ref, CuTAPI.toml.decodeFromString(dataAsString))) + } + } +} + // processes all resources and generates new resources based on the generate blocks internal fun generateResources(resources: List, stage: ResourceGenerationStage) { for (resource in resources) { - val generators = resource.metadata?.generateBlock ?: continue + if (resource is GenerateResource) { + // If the resource is a GenerateResource, we handle it separately + val generator = ResourceGenerator.getOrNull(resource.metadata.generatorId) + ?: error("'${resource.metadata.generatorId}' is not a valid Resource Generator for ${resource::class.simpleName}.") + if (generator.stage != stage) continue + + val ref = resource.ref.toString().removeSuffix("gen.toml") + resource.ref.extension + + val generateBlock = object : GenerateBlock() { + override val generatorId: Identifier = resource.metadata.generatorId + override val subId: String? = null + override val options: TomlTable = resource.metadata.options + } + + val newResources = mutableListOf() + fun register(resource: Resource) { + CuTAPI.resourceManager.register(resource) + newResources.add(resource) + } + + val baseResource = resource.metadata.baseId.getResource() + ?: error("Base resource for GenerateResource '${resource.ref}' not found.") + + val ctx = ResourceGeneratorContext(baseResource, generateBlock, ref(ref), ::register) + + generator.generate(ctx) + + for (newResource in newResources) { + generateResources(newResources, stage) + } + continue + } + + val generators = resource.metadata?.generateBlocks ?: continue for (generateBlock in generators) { try { - val generator = ResourceGenerator.getOrNull(generateBlock.generatorId) ?: error("'${generateBlock.generatorId}' is not a valid Resource Generator for ${resource::class.simpleName}.") if (generator.stage != stage) return @@ -22,7 +84,8 @@ internal fun generateResources(resources: List, stage: ResourceGenerat newResources.add(resource) } - val ctx = ResourceGeneratorContext(resource, generateBlock, subId, ::register) + val ctx = + ResourceGeneratorContext(resource, generateBlock, resource.ref.generatedSubId(subId), ::register) generator.generate(ctx) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/HorizontalAtlasTextureGenerator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/HorizontalAtlasTextureGenerator.kt index 4dba7d5..0cad6a5 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/HorizontalAtlasTextureGenerator.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/HorizontalAtlasTextureGenerator.kt @@ -34,18 +34,18 @@ public val HorizontalAtlasTextureGenerator: ResourceGenerator = resourceGenerato val amountOfTextures = texture.data.width / width - val subIdTemplate = this.suppliedSubId + val template = this.ref.toString() for (i in 0 until amountOfTextures) { val subTexture = texture.data.getSubimage(i * width, 0, width, texture.data.height).copy() - val subId = subIdTemplate.replace("#", i.toString()) + val ref = template.replace("*", i.toString()) var metadata = options.generatedMetadata.copy() if (options.specificMetadata.containsKey(i)) { metadata = metadata.apply(options.specificMetadata[i]!!, Texture2D.Metadata.serializer()) } val subTextureResource = Texture2D( - ref = texture.ref.generatedSubId(subId), + ref = ref(ref), data = subTexture, metadata = metadata ) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/InventoryTextureGenerator.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/InventoryTextureGenerator.kt new file mode 100644 index 0000000..007c037 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/InventoryTextureGenerator.kt @@ -0,0 +1,38 @@ +package xyz.mastriel.cutapi.resources.process + +import kotlinx.serialization.* +import xyz.mastriel.cutapi.* +import xyz.mastriel.cutapi.registry.* +import xyz.mastriel.cutapi.resources.* +import xyz.mastriel.cutapi.resources.builtin.* + +@Serializable +private data class InventoryTextureGeneratorOptions( + val texture: ResourceRef<@Contextual Texture2D> +) + +public val InventoryTextureGenerator: ResourceGenerator = resourceGenerator( + id(Plugin, "inventory_texture"), + ResourceGenerationStage.BeforeProcessors +) { + + val options = castOptions(InventoryTextureGeneratorOptions.serializer()) + + val model = ref(Plugin, "ui/inventory_bg.model3d.json").getResource()!! + + val newModel = Model3D( + ref = ref.cast(), + modelJson = model.modelJson.copy( + textures = model.modelJson.textures.toMutableMap().also { + it["2"] = options.texture.toMinecraftLocator() + } + ), + metadata = model.metadata.copy( + textures = model.metadata.textures.toMutableMap().also { + it["2"] = options.texture + } + ) + ) + + register(newModel) +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/Texture2DProcessor.kt b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/Texture2DProcessor.kt index 9f84834..881eddb 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/resources/process/Texture2DProcessor.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/resources/process/Texture2DProcessor.kt @@ -14,13 +14,18 @@ import kotlin.math.* public val TextureAndModelProcessor: ResourceProcessor = resourceProcessor { val textures = this.resources.filterIsInstance().filter { !it.metadata.transient } val models = this.resources.filterIsInstance() - val resourcePackManager = CuTAPI.resourcePackManager - - val namespaces = textures.map { it.ref.namespace }.distinct() + // puts the textures in the right places generateTexturesInPack(textures) + + // generates the item model and model files + generateTextureItemAndModelJsonFiles(textures) + + // puts the models in the right places generateModelsInPack(models) - generateItemAndModelJsonFiles(textures) + generateModelItemFiles(models) + + // generates the glyphs generateGlyphs(textures) } @@ -55,7 +60,7 @@ internal fun BitmapFontProvider( ): MinecraftFontProvider { // why does this need the extension exactly?? // it wont work without it. - val path = ref.path(withExtension = true, withNamespaceAsFolder = false).fixInvalidResourcePath() + val path = ref.path(withExtension = true, withNamespaceAsFolder = false, fixInvalids = true) val filePath = "${ref.namespace}:item/$path" return MinecraftFontProvider("bitmap", filePath, ascent, height, chars) } @@ -124,11 +129,11 @@ internal fun generateGlyphs(textures: List) { ascent = min(ascent, finalHeight) val privateUseChar = Character.toChars(privateUseCharIndex).joinToString("") - privateUseCharIndex += 1; + privateUseCharIndex += 1 if (fontSettings.advance != null) { val advance = fontSettings.advance val spaceChar = Character.toChars(privateUseCharIndex).joinToString("") - privateUseCharIndex += 1; + privateUseCharIndex += 1 spaceProvider.advances!![spaceChar] = advance texture.glyphChars[size] = spaceChar + privateUseChar } else { @@ -155,7 +160,7 @@ internal fun generateTexturesInPack(textures: List) { val texturesFolder = CuTAPI.resourcePackManager.getTexturesFolder(texture.ref.plugin) applyPostProcessing(texture) - texture.saveTo(texturesFolder appendPath texture.ref.path(withExtension = true).fixInvalidResourcePath()) + texture.saveTo(texturesFolder appendPath texture.ref.path(withExtension = true, fixInvalids = true)) val metadata = texture.metadata val animationData = metadata.animation @@ -173,9 +178,10 @@ internal fun generateTexturesInPack(textures: List) { val path = texture.ref.path( withExtension = false, withNamespaceAsFolder = false, - withNamespace = false - ).fixInvalidResourcePath() - val modelFile = CuTAPI.resourcePackManager.getItemModelFolder(texture.root.namespace) appendPath "/$path.json" + withNamespace = false, + fixInvalids = true + ) + val modelFile = CuTAPI.resourcePackManager.getModelFolder(texture.root.namespace) appendPath "/$path.json" modelFile.mkdirsOfParent() modelFile.createAndWrite(jsonString) @@ -185,8 +191,8 @@ internal fun generateTexturesInPack(textures: List) { internal fun generateModelsInPack(models: List) { // for this one we only need to put the models in the correct folder. models.forEach { model -> - val modelsFolder = CuTAPI.resourcePackManager.getItemModelFolder(model.ref.root.namespace) - model.saveTo(modelsFolder appendPath model.ref.path(withExtension = false).fixInvalidResourcePath() + ".json") + val modelsFolder = CuTAPI.resourcePackManager.getModelFolder(model.ref.root.namespace) + model.saveTo(modelsFolder appendPath model.ref.path(withExtension = false, fixInvalids = true) + ".json") } } @@ -206,15 +212,15 @@ private fun applyPostProcessing(texture: Texture2D) { } private fun generateModelFile(texture: Texture2D) { - val location = texture.getItemModel().location + val location = texture.getItemModel().getLocationWithItemFolder() val itemModelJson = texture.metadata.itemModelData?.copy( - textures = texture.metadata.itemModelData.textures + mapOf("layer0" to location) - ) ?: ItemModelData(parent = "minecraft:item/generated", textures = mapOf("layer0" to location)) + _textures = texture.metadata.itemModelData.textures + mapOf("layer0" to location) + ) ?: ItemModelData(parent = "minecraft:item/generated", _textures = mapOf("layer0" to location)) val jsonString = CuTAPI.json.encodeToString(itemModelJson) - val path = texture.ref.path(withExtension = false) + val path = texture.ref.path(withExtension = false, fixInvalids = true) val file = CuTAPI.resourcePackManager.getModelFolder(texture.ref.namespace) appendPath "${path}.json" @@ -226,19 +232,55 @@ private fun generateModelFile(texture: Texture2D) { private data class ItemModelFile( @SerialName("hand_animation_on_swap") val handAnimationOnSwap: Boolean, + val model: ModelData +) { + @Serializable + enum class ModelType { + @SerialName("minecraft:model") + Model + } + + @Serializable + data class ModelData( + val type: ModelType, + val model: VanillaRef ) +} // we generate 2 different item models, one with no hand swap animation and one with it -private fun generateItemModelFile(texture: Texture2D) { +private fun generateItemModelFiles(texture: TextureLike) { + + val modelData = ItemModelFile.ModelData( + ItemModelFile.ModelType.Model, + VanillaRef(texture.getItemModel().location) + ) + + val noSwap = ItemModelFile(false, modelData) + val swap = ItemModelFile(true, modelData) + val folder = CuTAPI.resourcePackManager.getItemModelFolder(texture.ref.namespace) + val path = texture.ref.path(withExtension = false) + val noSwapFile = folder appendPath "${path}__noswap.json" + val swapFile = folder appendPath "${path}__swap.json" + + noSwapFile.parentFile.mkdirs() + swapFile.parentFile.mkdirs() + noSwapFile.createAndWrite(CuTAPI.json.encodeToString(noSwap)) + swapFile.createAndWrite(CuTAPI.json.encodeToString(swap)) } -private fun generateItemAndModelJsonFiles(textures: Collection) { +private fun generateTextureItemAndModelJsonFiles(textures: Collection) { for (texture in textures) { generateModelFile(texture) + generateItemModelFiles(texture) + } +} +private fun generateModelItemFiles(textures: Collection) { + for (texture in textures) { + generateItemModelFiles(texture) } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/ConfigUtils.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/ConfigUtils.kt index 177b408..e8c2916 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/utils/ConfigUtils.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/ConfigUtils.kt @@ -9,15 +9,20 @@ import kotlin.reflect.* public fun configValue(plugin: Plugin, path: String, default: T): ConfigDelegate { return ConfigDelegate(plugin, path, default) } -internal fun cutConfigValue(path: String, default: T): ConfigDelegate { + +internal fun cutConfigValue(path: String, default: T): ConfigDelegate { return ConfigDelegate(Plugin, path, default) } -public class ConfigDelegate internal constructor(public val plugin: Plugin, public val path: String, public val default: T) : +public class ConfigDelegate internal constructor( + public val plugin: Plugin, + public val path: String, + public val default: T +) : ReadOnlyProperty { @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: Any?, property: KProperty<*>): T { + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { return try { (plugin.config.get(path) as? T?) ?: default } catch (ex: ClassCastException) { diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/Logging.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/Logging.kt new file mode 100644 index 0000000..d6cf9da --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/Logging.kt @@ -0,0 +1,66 @@ +package xyz.mastriel.cutapi.utils + +import java.util.logging.* + +public enum class LogLevel { + Trace, + Debug, + Info, + Warn, + Error +} + +public interface CuTLogger { + public fun log(level: LogLevel, message: String) + + public fun create(name: String): CuTLogger { + return logger(this, name) + } + + public val names: List +} + +public fun CuTLogger.trace(message: String) { + log(LogLevel.Trace, message) +} + +public fun CuTLogger.debug(message: String) { + log(LogLevel.Debug, message) +} + +public fun CuTLogger.info(message: String) { + log(LogLevel.Info, message) +} + +public fun CuTLogger.warn(message: String) { + log(LogLevel.Warn, message) +} + +public fun CuTLogger.error(message: String) { + log(LogLevel.Error, message) +} + +private fun logger(parent: CuTLogger?, name: String): CuTLogger { + return object : CuTLogger { + override fun log(level: LogLevel, message: String) { + val logName = names.joinToString( + separator = "/" + ) + + val logger = Logger.getLogger(logName) + when (level) { + LogLevel.Trace -> logger.finer(message) + LogLevel.Debug -> logger.fine(message) + LogLevel.Info -> logger.info(message) + LogLevel.Warn -> logger.warning(message) + LogLevel.Error -> logger.severe(message) + } + } + + override val names: List + get() = (parent?.names ?: listOf()) + listOf(name) + } +} + +internal object RootLogger : CuTLogger by logger(null, "CuTAPI") { +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/PlayerUtils.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/PlayerUtils.kt index 25230ab..b95fb3f 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/utils/PlayerUtils.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/PlayerUtils.kt @@ -2,6 +2,9 @@ package xyz.mastriel.cutapi.utils import org.bukkit.* import org.bukkit.entity.* +import java.util.* +import kotlin.properties.* +import kotlin.reflect.* public fun onlinePlayers(): List = Bukkit.getOnlinePlayers() @@ -9,3 +12,16 @@ public fun onlinePlayers(): List = Bukkit.getOnlinePlayers() .toList() public fun playerNameList(): List = onlinePlayers().map(Player::getName) + + +@JvmInline +public value class PlayerUUID(public val uuid: UUID) : ReadOnlyProperty { + public fun toPlayer(): Player? = Bukkit.getPlayer(uuid) + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): Player? { + return toPlayer() + } + +} + +public val Player.playerUUID: PlayerUUID get() = PlayerUUID(uniqueId) \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/SimpleEventHandler.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/SimpleEventHandler.kt index 730c7a7..b4efeaa 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/utils/SimpleEventHandler.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/SimpleEventHandler.kt @@ -19,6 +19,8 @@ public class EventHandlerList { public fun trigger(event: TInput): Unit = events.forEach { with(it) { event.trigger() } } - public operator fun invoke(event: TInput): Unit = trigger(event) + public operator fun invoke(event: SimpleEventHandler) { + events += event + } } diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/TomlJsonUtils.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/TomlJsonUtils.kt index 5a0e3c4..b3cbfc8 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/utils/TomlJsonUtils.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/TomlJsonUtils.kt @@ -67,7 +67,9 @@ public fun TomlTable.combine(other: TomlTable, combineArrays: Boolean = true): T if (map[key] is TomlArray && other[key] is TomlArray) { val newArray = mutableListOf() val (currentArray, otherArray) = map[key]!!.asTomlArray() to other[key]!!.asTomlArray() - if (combineArrays) newArray.addAll(currentArray) + if (combineArrays || otherArray.isEmpty()) { + newArray.addAll(currentArray) + } newArray.addAll(otherArray) map[key] = TomlArray(newArray) continue diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/WeakPlayerMap.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/WeakPlayerMap.kt new file mode 100644 index 0000000..edd7aa7 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/WeakPlayerMap.kt @@ -0,0 +1,66 @@ +package xyz.mastriel.cutapi.utils + +import org.bukkit.* +import org.bukkit.entity.* + +// A map that only allows for online players to be stored. +public class WeakPlayerMap internal constructor(private val base: MutableMap) : MutableMap { + override fun put(key: Player, value: V): V? { + purgeOfflinePlayers() + return base.put(key, value) + } + + + private fun purgeOfflinePlayers() { + base.keys.removeIf { !it.isOnline } + } + + override val entries: MutableSet> + get() = purgeOfflinePlayers().let { base.entries } + override val keys: MutableSet + get() = purgeOfflinePlayers().let { base.keys } + override val size: Int + get() = purgeOfflinePlayers().let { base.size } + override val values: MutableCollection + get() = purgeOfflinePlayers().let { base.values } + + override fun clear() { + return purgeOfflinePlayers().let { base.clear() } + } + + override fun isEmpty(): Boolean { + return purgeOfflinePlayers().let { base.isEmpty() } + } + + override fun remove(key: Player): V? { + return purgeOfflinePlayers().let { base.remove(key) } + } + + override fun putAll(from: Map) { + return purgeOfflinePlayers().let { base.putAll(from) } + } + + override fun get(key: Player): V? { + return purgeOfflinePlayers().let { base[key] } + } + + override fun containsValue(value: V): Boolean { + return purgeOfflinePlayers().let { base.containsValue(value) } + } + + override fun containsKey(key: Player): Boolean { + return purgeOfflinePlayers().let { base.containsKey(key) } + } +} + +public fun weakPlayerMapOf(): WeakPlayerMap = WeakPlayerMap(mutableMapOf()) + +public fun weakPlayerMapOf(vararg pairs: Pair): WeakPlayerMap = WeakPlayerMap(mutableMapOf(*pairs)) + +public fun weakPlayerMapOf(map: Map): WeakPlayerMap = WeakPlayerMap(map.toMutableMap()) + +public fun MutableMap.toWeakPlayerMap(): WeakPlayerMap = WeakPlayerMap(this) + +@JvmName("toWeakPlayerMapFromOfflinePlayer") +public fun MutableMap.toWeakPlayerMap(): WeakPlayerMap = + WeakPlayerMap(this.filterKeys { it.isOnline }.mapKeys { it.key.player!! }.toMutableMap()) diff --git a/src/main/kotlin/xyz/mastriel/cutapi/utils/serializers/LocationSerializer.kt b/src/main/kotlin/xyz/mastriel/cutapi/utils/serializers/LocationSerializer.kt index 6fe8014..46cabc0 100644 --- a/src/main/kotlin/xyz/mastriel/cutapi/utils/serializers/LocationSerializer.kt +++ b/src/main/kotlin/xyz/mastriel/cutapi/utils/serializers/LocationSerializer.kt @@ -30,7 +30,7 @@ public object LocationSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("Location") { + buildClassSerialDescriptor("cutapi:location") { element("x") element("y") element("z") diff --git a/src/main/kotlin/xyz/mastriel/cutapi/world/CuTChunk.kt b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTChunk.kt new file mode 100644 index 0000000..6569432 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTChunk.kt @@ -0,0 +1,6 @@ +package xyz.mastriel.cutapi.world + +import org.bukkit.* + +public class CuTChunk(public val handle: Chunk) { +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorld.kt b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorld.kt new file mode 100644 index 0000000..618fe31 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorld.kt @@ -0,0 +1,20 @@ +package xyz.mastriel.cutapi.world + +import org.bukkit.* +import xyz.mastriel.cutapi.* + +public class CuTWorld(public val handle: World) { + + + public fun cleanup() { + + } + + public fun initialize() { + + } +} + +public fun World.wrap() { + CuTAPI +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorldManager.kt b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorldManager.kt new file mode 100644 index 0000000..23fc7f0 --- /dev/null +++ b/src/main/kotlin/xyz/mastriel/cutapi/world/CuTWorldManager.kt @@ -0,0 +1,49 @@ +package xyz.mastriel.cutapi.world + +import org.bukkit.* +import org.bukkit.event.* +import org.bukkit.event.world.* + +public class CuTWorldManager : Listener { + + private val worlds = mutableMapOf() + + @EventHandler + internal fun onWorldLoad(event: WorldLoadEvent) { + initWorld(event.world) + } + + @EventHandler + internal fun onWorldUnload(event: WorldUnloadEvent) { + uninitWorld(event.world) + } + + private fun initWorld(world: World) { + if (worlds.containsKey(world)) { + throw IllegalArgumentException("World ${world.name} is already initialized in CuTWorldManager!") + } + CuTWorld(world).also { + worlds[world] = it + it.initialize() + } + } + + private fun uninitWorld(world: World) { + worlds[world]?.cleanup(); + worlds.remove(world); + } + + public fun getCuTWorld(world: World): CuTWorld { + return worlds.getOrElse(world) { + throw IllegalArgumentException("World ${world.name} is not initialized in CuTWorldManager!") + } + } + + public fun getWorlds(): Collection { + return worlds.values + } + + public fun getWorldByType(type: World.Environment): CuTWorld? { + return worlds.values.firstOrNull { it.handle.environment == type } + } +} \ No newline at end of file diff --git a/src/main/resources/pack/ui/inventory_bg.model3d.json b/src/main/resources/pack/ui/inventory_bg.model3d.json index 5d3d80f..9032f45 100644 --- a/src/main/resources/pack/ui/inventory_bg.model3d.json +++ b/src/main/resources/pack/ui/inventory_bg.model3d.json @@ -1,56 +1,211 @@ { "credit": "Made with Blockbench", "textures": { - "2": "item/inventory_part" }, "elements": [ { - "from": [0, 0, -1], - "to": [16, 16, 0], - "rotation": {"angle": 0, "axis": "y", "origin": [8, 8, 7]}, + "from": [ + 1.0435, + 1.0435, + -1 + ], + "to": [ + 14.95654, + 14.95654, + 0 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 8, + 8, + 7 + ] + }, "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, - "east": {"uv": [0, 0, 16, 1], "texture": "#1"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, - "west": {"uv": [0, 0, 0, 0], "texture": "#1"}, - "up": {"uv": [0, 0, 0, 0], "texture": "#1"}, - "down": {"uv": [0, 0, 0, 0], "texture": "#1"} + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#1" + }, + "east": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#1" + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#1" + }, + "west": { + "uv": [ + 0, + 0, + 0, + 0 + ], + "texture": "#1" + }, + "up": { + "uv": [ + 0, + 0, + 0, + 0 + ], + "texture": "#1" + }, + "down": { + "uv": [ + 0, + 0, + 0, + 0 + ], + "texture": "#1" + } } }, { - "from": [0, 0, 0], - "to": [16, 16, 16], - "rotation": {"angle": 0, "axis": "y", "origin": [8, 8, 23]}, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 8, + 8, + 23 + ] + }, "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 1], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 1], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 1], "texture": "#2"}, - "up": {"uv": [0, 0, 0, 0], "texture": "#2"}, - "down": {"uv": [0, 0, 16, 16], "texture": "#2"} + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 0, + 0, + 16, + 1 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 0, + 0, + 0, + 0 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#2" + } } } ], "gui_light": "front", "display": { "thirdperson_righthand": { - "scale": [0, 0, 0] + "scale": [ + 0, + 0, + 0 + ] }, "thirdperson_lefthand": { - "scale": [0, 0, 0] + "scale": [ + 0, + 0, + 0 + ] }, "firstperson_righthand": { - "scale": [0, 0, 0] + "scale": [ + 0, + 0, + 0 + ] }, "firstperson_lefthand": { - "scale": [0, 0, 0] + "scale": [ + 0, + 0, + 0 + ] }, "ground": { - "scale": [0.3, 0.3, 0.3] + "scale": [ + 0.3, + 0.3, + 0.3 + ] }, "gui": { - "rotation": [-180, -180, 0], - "scale": [1.15, -1.15, -1.15] + "rotation": [ + -180, + -180, + 0 + ], + "scale": [ + 1.15, + -1.15, + -1.15 + ] } } } \ No newline at end of file diff --git a/src/main/resources/pack/ui/inventory_part.model3d.json b/src/main/resources/pack/ui/inventory_part.model3d.json deleted file mode 100644 index ba74fa2..0000000 --- a/src/main/resources/pack/ui/inventory_part.model3d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "item/icon/template", - "textures": { - "1": "item/inventory_part" - } -} \ No newline at end of file diff --git a/src/main/resources/pack/ui/inventory_part.model3d.json.meta b/src/main/resources/pack/ui/inventory_part.model3d.json.meta deleted file mode 100644 index 14f4e9f..0000000 --- a/src/main/resources/pack/ui/inventory_part.model3d.json.meta +++ /dev/null @@ -1,3 +0,0 @@ - -id = "cutapi:model3d" -materials = ["glistering_melon_slice"] diff --git a/src/main/resources/pack/ui/inventory_texture.template b/src/main/resources/pack/ui/inventory_texture.template new file mode 100644 index 0000000..cd2d161 --- /dev/null +++ b/src/main/resources/pack/ui/inventory_texture.template @@ -0,0 +1,4 @@ +[item_model_data] +parent = "cutapi:ui/inventory_bg" +textures.1 = "{{@ref}}" +textures.2 = "cutapi://ui/inventory_bg.png" diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 4906b27..cf15339 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -2,7 +2,7 @@ name: CuTAPI version: '$version' main: xyz.mastriel.cutapi.CuTAPIPlugin description: An API designed to make creating cool things in plugins possible! -api-version: '1.20' +api-version: '1.21' loader: xyz.mastriel.cutapi.CuTAPILoader permissions: cutapi.admin.give: