diff --git a/build.gradle.kts b/build.gradle.kts index 3e98c8e..8468e11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,12 +29,6 @@ allprojects { options.release = 21 } - val mod = ModInfo() - val deps = Dependencies() - - group = mod.group - version = mod.version - java { withSourcesJar() @@ -42,10 +36,11 @@ allprojects { targetCompatibility = JavaVersion.VERSION_21 } - loom { - splitEnvironmentSourceSets() - accessWidenerPath = rootDir.absoluteFile.resolve("src/main/resources/specter.accesswidener") - } + val mod = ModInfo() + val deps = Dependencies() + + group = mod.group + version = mod.version dependencies { minecraft("com.mojang:minecraft:${deps.minecraft}") @@ -55,18 +50,6 @@ allprojects { modImplementation("net.fabricmc.fabric-api:fabric-api:${deps.fabricApi}") } - for (modProject in allprojects) { - loom.mods.register(modProject.name) { - sourceSet(modProject.sourceSets.getByName("main")) - sourceSet(modProject.sourceSets.getByName("client")) - } - - loom.mods.register(modProject.name + "-testmod") { - sourceSet(modProject.sourceSets.getByName("testmod")) - sourceSet(modProject.sourceSets.getByName("testmodClient")) - } - } - tasks.withType { inputs.property("id", mod.id) inputs.property("version", mod.version) @@ -83,6 +66,23 @@ allprojects { filesMatching("fabric.mod.json") { expand(map) } } + loom { + splitEnvironmentSourceSets() + accessWidenerPath = rootDir.absoluteFile.resolve("src/main/resources/specter.accesswidener") + } + + for (modProject in allprojects) { + loom.mods.register(modProject.name) { + sourceSet(modProject.sourceSets.getByName("main")) + sourceSet(modProject.sourceSets.getByName("client")) + } + + loom.mods.register(modProject.name + "-testmod") { + sourceSet(modProject.sourceSets.getByName("testmod")) + sourceSet(modProject.sourceSets.getByName("testmodClient")) + } + } + tasks.withType().configureEach { enabled = false } @@ -90,13 +90,14 @@ allprojects { tasks.javadoc { with(options as StandardJavadocDocletOptions) { + source = "21" encoding = "UTF-8" charset("UTF-8") memberLevel = JavadocMemberLevel.PACKAGE addStringOption("Xdoclint:none", "-quiet") } - allprojects.forEach { p -> source(p.sourceSets.main.map { it.allJava.srcDirs }) } + allprojects.forEach { proj -> source(proj.sourceSets.main.map { it.allJava.srcDirs }) } classpath = project.files(sourceSets.main.map { it.compileClasspath }) include("**/api/**") @@ -106,14 +107,21 @@ tasks.javadoc { val javadocJar by tasks.registering(Jar::class) { dependsOn(tasks.javadoc) from(tasks.javadoc.map { it.destinationDir!! }) - archiveClassifier.set("javadoc") + + archiveClassifier.set("fatjavadoc") } tasks.assemble.configure { dependsOn(javadocJar) } +tasks.test.configure { + dependsOn(tasks.named("runGametest")) +} + subprojects { + version = rootProject.version + sourceSets.create("testmod") { compileClasspath += sourceSets.main.get().compileClasspath runtimeClasspath += sourceSets.main.get().runtimeClasspath @@ -264,8 +272,4 @@ dependencies { } } -tasks.test { - dependsOn(":runGametest") -} - for (subproject in subprojects) tasks.remapJar.configure { dependsOn(":${subproject.name}:remapJar") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 70b3642..b7c6fad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,4 +14,5 @@ include("specter-config") include("specter-registry") include("specter-item") include("specter-block") +include("specter-entity") include("specter-debug") diff --git a/specter-entity/build.gradle.kts b/specter-entity/build.gradle.kts new file mode 100644 index 0000000..2c4c1cd --- /dev/null +++ b/specter-entity/build.gradle.kts @@ -0,0 +1 @@ +moduleDependencies(project, "specter-core", "specter-registry") diff --git a/specter-entity/src/client/resources/specter-entity.client.mixins.json b/specter-entity/src/client/resources/specter-entity.client.mixins.json new file mode 100644 index 0000000..7883fe2 --- /dev/null +++ b/specter-entity/src/client/resources/specter-entity.client.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.spiritstudios.specter.mixin.entity.client", + "compatibilityLevel": "JAVA_21", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + ] +} diff --git a/specter-entity/src/main/java/dev/spiritstudios/specter/api/entity/EntityAttachments.java b/specter-entity/src/main/java/dev/spiritstudios/specter/api/entity/EntityAttachments.java new file mode 100644 index 0000000..500a211 --- /dev/null +++ b/specter-entity/src/main/java/dev/spiritstudios/specter/api/entity/EntityAttachments.java @@ -0,0 +1,29 @@ +package dev.spiritstudios.specter.api.entity; + +import dev.spiritstudios.specter.api.registry.attachment.Attachment; +import dev.spiritstudios.specter.impl.entity.DataDefaultAttributeBuilder; +import net.minecraft.entity.EntityType; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +import static dev.spiritstudios.specter.api.core.SpecterGlobals.MODID; + +public final class EntityAttachments { + public static final Attachment, DataDefaultAttributeBuilder> DEFAULT_ATTRIBUTES = Attachment.builder( + Registries.ENTITY_TYPE, + Identifier.of(MODID, "default_attributes"), + DataDefaultAttributeBuilder.CODEC, + DataDefaultAttributeBuilder.PACKET_CODEC + ).build(); + + /** + * Hacky workaround to force class loading. + */ + @SuppressWarnings("EmptyMethod") + public static void init() { + // NO-OP + } + + private EntityAttachments() { + } +} diff --git a/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/DataDefaultAttributeBuilder.java b/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/DataDefaultAttributeBuilder.java new file mode 100644 index 0000000..5b66674 --- /dev/null +++ b/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/DataDefaultAttributeBuilder.java @@ -0,0 +1,45 @@ +package dev.spiritstudios.specter.impl.entity; + +import com.mojang.serialization.Codec; +import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; +import net.minecraft.entity.attribute.DefaultAttributeContainer; +import net.minecraft.entity.attribute.EntityAttribute; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; + +import java.util.Map; + +// Yes, this is a builder builder. I love codecs. +public record DataDefaultAttributeBuilder(Map, Double> attributes) { + public DefaultAttributeContainer build() { + DefaultAttributeContainer.Builder builder = DefaultAttributeContainer.builder(); + attributes.forEach(builder::add); + return builder.build(); + } + + public static final Codec CODEC = Codec.unboundedMap( + Registries.ATTRIBUTE.getEntryCodec(), + Codec.DOUBLE + ).xmap(DataDefaultAttributeBuilder::new, DataDefaultAttributeBuilder::attributes); + + public static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + PacketCodecs.map( + Object2DoubleOpenHashMap::new, + PacketCodecs.registryEntry(RegistryKeys.ATTRIBUTE), + PacketCodecs.DOUBLE + ), + DataDefaultAttributeBuilder::attributes, + DataDefaultAttributeBuilder::new + ); + + public static DataDefaultAttributeBuilder with(DataDefaultAttributeBuilder original, DefaultAttributeContainer attributes) { + Map, Double> newAttributes = new Object2DoubleOpenHashMap<>(); + attributes.instances.forEach((attribute, value) -> newAttributes.put(attribute, value.getBaseValue())); + newAttributes.putAll(original.attributes); + return new DataDefaultAttributeBuilder(newAttributes); + } +} diff --git a/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/SpecterEntity.java b/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/SpecterEntity.java new file mode 100644 index 0000000..b4d5900 --- /dev/null +++ b/specter-entity/src/main/java/dev/spiritstudios/specter/impl/entity/SpecterEntity.java @@ -0,0 +1,11 @@ +package dev.spiritstudios.specter.impl.entity; + +import dev.spiritstudios.specter.api.entity.EntityAttachments; +import net.fabricmc.api.ModInitializer; + +public class SpecterEntity implements ModInitializer { + @Override + public void onInitialize() { + EntityAttachments.init(); + } +} diff --git a/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java b/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java new file mode 100644 index 0000000..c2bf40e --- /dev/null +++ b/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java @@ -0,0 +1,40 @@ +package dev.spiritstudios.specter.mixin.entity; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.spiritstudios.specter.api.entity.EntityAttachments; +import dev.spiritstudios.specter.impl.entity.DataDefaultAttributeBuilder; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.attribute.DefaultAttributeContainer; +import net.minecraft.entity.attribute.DefaultAttributeRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Mixin(DefaultAttributeRegistry.class) +public class DefaultAttributeRegistryMixin { + @SuppressWarnings("unchecked") + @WrapOperation(method = "get", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;")) + private static V get(Map instance, Object o, Operation original) { + if (!(o instanceof EntityType entityType)) return original.call(instance, o); + + Optional attributeBuilder = EntityAttachments.DEFAULT_ATTRIBUTES.get(entityType); + if (attributeBuilder.isEmpty()) return original.call(instance, o); + + DefaultAttributeContainer originalAttributes = (DefaultAttributeContainer) original.call(instance, o); + if (originalAttributes == null) return (V) attributeBuilder.get().build(); + + return (V) DataDefaultAttributeBuilder.with(attributeBuilder.get(), originalAttributes).build(); + } + + @WrapOperation(method = "hasDefinitionFor", at = @At(value = "INVOKE", target = "Ljava/util/Map;containsKey(Ljava/lang/Object;)Z")) + private static boolean containsKey(Map instance, Object o, Operation original) { + if (!(o instanceof EntityType entityType)) return original.call(instance, o); + + boolean hasDefinition = Objects.nonNull(EntityAttachments.DEFAULT_ATTRIBUTES.get(entityType)); + return hasDefinition || original.call(instance, o); + } +} diff --git a/specter-entity/src/main/resources/fabric.mod.json b/specter-entity/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..b238d69 --- /dev/null +++ b/specter-entity/src/main/resources/fabric.mod.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "id": "specter-entity", + "version": "${version}", + "name": "Specter Entity", + "description": "Library for Spirit Studios mods (Entity module)", + "authors": [], + "contact": { + "repo": "https://github.com/SpiritGameStudios/Specter", + "issues": "https://github.com/SpiritGameStudios/Specter/issues" + }, + "license": "MPL-2.0", + "environment": "*", + "entrypoints": { + "main": [ + "dev.spiritstudios.specter.impl.entity.SpecterEntity" + ] + }, + "mixins": [ + "specter-entity.mixins.json", + { + "config": "specter-entity.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=${loader_version}", + "minecraft": "~${minecraft_version}", + "fabric-api": "*", + "java": ">=21" + }, + "custom": { + "modmenu": { + "parent": "specter", + "badges": [ + "library" + ] + } + } +} diff --git a/specter-entity/src/main/resources/specter-entity.mixins.json b/specter-entity/src/main/resources/specter-entity.mixins.json new file mode 100644 index 0000000..4621d7d --- /dev/null +++ b/specter-entity/src/main/resources/specter-entity.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.spiritstudios.specter.mixin.entity", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "DefaultAttributeRegistryMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/specter-entity/src/testmod/resources/data/specter/attachments/minecraft/entity_type/default_attributes.json b/specter-entity/src/testmod/resources/data/specter/attachments/minecraft/entity_type/default_attributes.json new file mode 100644 index 0000000..47cdc49 --- /dev/null +++ b/specter-entity/src/testmod/resources/data/specter/attachments/minecraft/entity_type/default_attributes.json @@ -0,0 +1,8 @@ +{ + "replace": false, + "values": { + "minecraft:warden": { + "minecraft:generic.max_health": 1.0 + } + } +} diff --git a/specter-entity/src/testmod/resources/fabric.mod.json b/specter-entity/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000..8c363a0 --- /dev/null +++ b/specter-entity/src/testmod/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "specter-entity-testmod", + "version": "${version}", + "name": "Specter", + "description": "Library for Spirit Studios mods (Entity module tests)", + "authors": [], + "contact": { + "repo": "https://github.com/SpiritGameStudios/Specter", + "issues": "https://github.com/SpiritGameStudios/Specter/issues" + }, + "license": "MPL-2.0", + "environment": "*", + "entrypoints": { + }, + "depends": { + "fabricloader": ">=${loader_version}", + "minecraft": "~${minecraft_version}", + "fabric-api": "*", + "java": ">=21" + } +} diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java b/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java index 3c229b6..769f824 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java @@ -4,8 +4,8 @@ import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider; import net.minecraft.data.DataOutput; -import net.minecraft.item.Item; import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; import net.minecraft.registry.RegistryWrapper; import net.minecraft.util.Identifier; import org.jetbrains.annotations.ApiStatus; @@ -28,7 +28,7 @@ protected void configure(BiConsumer provider, Registr new DataItemGroup( id.toTranslationKey("item_group"), data.icon(), - data.items().stream().map(ItemConvertible::asItem).map(Item::getDefaultStack).toList() + data.items() ) ), lookup @@ -42,8 +42,8 @@ public String getName() { return "Specter Item Groups"; } - public record ItemGroupData(Identifier id, ItemConvertible icon, List items) { - public static ItemGroupData of(Identifier id, ItemConvertible icon, List items) { + public record ItemGroupData(Identifier id, ItemConvertible icon, List items) { + public static ItemGroupData of(Identifier id, ItemConvertible icon, List items) { return new ItemGroupData(id, icon, items); } } diff --git a/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/registration/SoundEventRegistrar.java b/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/registration/SoundEventRegistrar.java new file mode 100644 index 0000000..5e291cd --- /dev/null +++ b/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/registration/SoundEventRegistrar.java @@ -0,0 +1,18 @@ +package dev.spiritstudios.specter.api.registry.registration; + +import net.minecraft.registry.Registry; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; + +import java.lang.reflect.Field; + +/** + * Automatically register each sound event using reflection. + * Unlike normal registrars, - in the field name is replaced with . in the registry to allow for easier naming. + */ +public interface SoundEventRegistrar extends MinecraftRegistrar { + @Override + default void register(String name, String namespace, SoundEvent object, Field field) { + Registry.register(getRegistry(), Identifier.of(namespace, name.replace('-', '.')), object); + } +} diff --git a/src/main/resources/specter.accesswidener b/src/main/resources/specter.accesswidener index 7377413..46a14c2 100644 --- a/src/main/resources/specter.accesswidener +++ b/src/main/resources/specter.accesswidener @@ -4,3 +4,4 @@ accessible field net/minecraft/registry/Registries ROOT Lnet/minecraft/registry/ accessible class net/minecraft/block/ComposterBlock$ComposterInventory accessible method net/minecraft/item/ItemGroup (Lnet/minecraft/item/ItemGroup$Row;ILnet/minecraft/item/ItemGroup$Type;Lnet/minecraft/text/Text;Ljava/util/function/Supplier;Lnet/minecraft/item/ItemGroup$EntryCollector;)V accessible method net/minecraft/data/server/recipe/ShapedRecipeJsonBuilder validate (Lnet/minecraft/util/Identifier;)Lnet/minecraft/recipe/RawShapedRecipe; +accessible field net/minecraft/entity/attribute/DefaultAttributeContainer instances Ljava/util/Map;