diff --git a/fabric-data-attachment-api-v1/build.gradle b/fabric-data-attachment-api-v1/build.gradle index 10d208c1f1..cd51ebaf98 100644 --- a/fabric-data-attachment-api-v1/build.gradle +++ b/fabric-data-attachment-api-v1/build.gradle @@ -7,5 +7,6 @@ moduleDependencies(project, [ ]) testDependencies(project, [ - ':fabric-lifecycle-events-v1' + ':fabric-lifecycle-events-v1', + ':fabric-biome-api-v1' ]) diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java index 093082c148..c26be0d04d 100644 --- a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java @@ -38,7 +38,7 @@ private void copyAttachmentsOnClientRespawn(ClientPlayerEntity newPlayer, Operat /* * The KEEP_ATTRIBUTES flag is not set on a death respawn, and set in all other cases */ - AttachmentTargetImpl.copyOnRespawn(oldPlayer, newPlayer, !packet.hasFlag(PlayerRespawnS2CPacket.KEEP_ATTRIBUTES)); + AttachmentTargetImpl.transfer(oldPlayer, newPlayer, !packet.hasFlag(PlayerRespawnS2CPacket.KEEP_ATTRIBUTES)); init.call(newPlayer); } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java index 3cdf07dfcd..77dda86d7a 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java @@ -28,14 +28,14 @@ import net.minecraft.entity.Entity; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.ChunkStatus; /** * Marks all objects on which data can be attached using {@link AttachmentType}s. * - *

Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerWorld} and {@link WorldChunk} via mixin.

+ *

Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerWorld} and {@link Chunk} via mixin.

* - *

Note about {@link BlockEntity} and {@link WorldChunk} targets: these objects need to be notified of changes to their + *

Note about {@link BlockEntity} and {@link Chunk} targets: these objects need to be notified of changes to their * state (using {@link BlockEntity#markDirty()} and {@link Chunk#setNeedsSaving(boolean)} respectively), otherwise the modifications will not take effect properly. * The {@link #setAttached(AttachmentType, Object)} method handles this automatically, but this needs to be done manually * when attached data is mutable, for example: @@ -55,6 +55,12 @@ * which takes care of all vanilla types. However, modded block entities may be coded differently, so be wary of this * when attaching data to modded block entities. *

+ * + *

+ * Note about {@link Chunk} targets with {@link ChunkStatus#EMPTY}: These chunks are not saved unless the generation + * progresses to at least {@link ChunkStatus#STRUCTURE_STARTS}. Therefore, persistent attachments to those chunks may not + * be saved. The {@link #setAttached(AttachmentType, Object)} method will log a warning when this is attempted. + *

*/ @ApiStatus.Experimental @ApiStatus.NonExtendable diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java index e8d07e0283..cfae909490 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java @@ -23,11 +23,14 @@ import org.jetbrains.annotations.Nullable; import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ProtoChunk; +import net.minecraft.world.chunk.WorldChunk; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; /** @@ -39,13 +42,15 @@ * immutable types. More generally, different attachments must not share mutable state, and it is strongly advised * for attachments not to hold internal references to their target. See the following note on entity targets.

* - *

Note on {@link Entity} targets: in several instances, the name needs to copy data from one {@link Entity} to another. - * These are player respawning, mob conversion, return from the End and cross-world entity teleportation. By default, - * attachments are simply copied wholesale, up to {@link #copyOnDeath()}. Since one entity instance is discarded, - * an attachment that keeps a reference to an {@link Entity} instance can and will break unexpectedly. If, - * for whatever reason, keeping to reference to the target entity is absolutely necessary, be sure to use - * {@link ServerPlayerEvents#COPY_FROM}, {@link ServerEntityWorldChangeEvents#AFTER_ENTITY_CHANGE_WORLD} - * and a mixin into {@link MobEntity#convertTo(EntityType, boolean)} to implement custom copying logic.

+ *

Note on {@link Entity} and {@link Chunk} targets: in several instances, the game needs to copy data from one instance to another. + * These are player respawning, mob conversion, return from the End, cross-world entity teleportation, and conversion of a {@link ProtoChunk} to + * {@link WorldChunk}. By default, attachments are simply copied wholesale, up to {@link #copyOnDeath()}. Since one instance is discarded, + * an attachment that keeps a reference to an {@link Entity} or {@link ProtoChunk} instance can and will break unexpectedly. If, + * for whatever reason, keeping a reference to the target is absolutely necessary, be sure to implement custom copying logic. + * For {@link Entity} targets, use {@link ServerPlayerEvents#COPY_FROM}, {@link ServerEntityWorldChangeEvents#AFTER_ENTITY_CHANGE_WORLD}, + * and {@link ServerLivingEntityEvents#MOB_CONVERSION}. For {@link Chunk} targets, mixin into + * {@link WorldChunk#WorldChunk(ServerWorld, ProtoChunk, WorldChunk.EntityLoader)}. + *

* * @param type of the attached data. It is encouraged for this to be an immutable type. */ diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java index 92e848d6a0..dc5fa58108 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java @@ -16,23 +16,28 @@ package net.fabricmc.fabric.impl.attachment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; public class AttachmentEntrypoint implements ModInitializer { + public static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1"); + @Override public void onInitialize() { ServerPlayerEvents.COPY_FROM.register((oldPlayer, newPlayer, alive) -> - AttachmentTargetImpl.copyOnRespawn(oldPlayer, newPlayer, !alive) + AttachmentTargetImpl.transfer(oldPlayer, newPlayer, !alive) ); ServerEntityWorldChangeEvents.AFTER_ENTITY_CHANGE_WORLD.register(((originalEntity, newEntity, origin, destination) -> - AttachmentTargetImpl.copyOnRespawn(originalEntity, newEntity, false)) + AttachmentTargetImpl.transfer(originalEntity, newEntity, false)) ); // using the corresponding player event is unnecessary as no new instance is created ServerLivingEntityEvents.MOB_CONVERSION.register((previous, converted, keepEquipment) -> - AttachmentTargetImpl.copyOnRespawn(previous, converted, true) + AttachmentTargetImpl.transfer(previous, converted, true) ); } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java index c10d17f339..6cf81b0be0 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java @@ -28,12 +28,13 @@ public interface AttachmentTargetImpl extends AttachmentTarget { /** - * Copies entity attachments when it is respawned and a new instance is created. - * Is triggered on player respawn, entity conversion, return from the End or cross-world entity teleportation. + * Copies attachments from the original to the target. This is used when a ProtoChunk is converted to a + * WorldChunk, and when an entity is respawned and a new instance is created. For entity respawns, it is + * triggered on player respawn, entity conversion, return from the End, or cross-world entity teleportation. * In the first two cases, only the attachments with {@link AttachmentType#copyOnDeath()} will be transferred. - */ + */ @SuppressWarnings("unchecked") - static void copyOnRespawn(AttachmentTarget original, AttachmentTarget target, boolean isDeath) { + static void transfer(AttachmentTarget original, AttachmentTarget target, boolean isDeath) { Map, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments(); if (attachments == null) { diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java index bd1bef57cd..22acd8554b 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java @@ -28,13 +28,14 @@ import net.minecraft.registry.RegistryWrapper; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.ChunkStatus; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint; import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; -@Mixin({BlockEntity.class, Entity.class, World.class, WorldChunk.class}) +@Mixin({BlockEntity.class, Entity.class, World.class, Chunk.class}) abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Nullable private IdentityHashMap, Object> fabric_dataAttachments = null; @@ -55,8 +56,12 @@ public T setAttached(AttachmentType type, @Nullable T value) { if (thisObject instanceof BlockEntity) { ((BlockEntity) thisObject).markDirty(); - } else if (thisObject instanceof WorldChunk) { + } else if (thisObject instanceof Chunk) { ((Chunk) thisObject).setNeedsSaving(true); + + if (type.isPersistent() && ((Chunk) thisObject).getStatus().equals(ChunkStatus.EMPTY)) { + AttachmentEntrypoint.LOGGER.warn("Attaching persistent attachment {} to chunk with chunk status EMPTY. Attachment might be discarded.", type.identifier()); + } } if (value == null) { diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java index a06d8aed53..32d5d9a06f 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java @@ -27,7 +27,7 @@ import net.minecraft.util.math.ChunkPos; import net.minecraft.world.ChunkSerializer; import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.ProtoChunk; import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.poi.PointOfInterestStorage; @@ -42,7 +42,19 @@ abstract class ChunkSerializerMixin { ), method = "deserialize" ) - private static WorldChunk readChunkAttachments(WorldChunk chunk, ServerWorld world, PointOfInterestStorage poiStorage, ChunkPos chunkPos, NbtCompound nbt) { + private static WorldChunk readWorldChunkAttachments(WorldChunk chunk, ServerWorld world, PointOfInterestStorage poiStorage, ChunkPos chunkPos, NbtCompound nbt) { + ((AttachmentTargetImpl) chunk).fabric_readAttachmentsFromNbt(nbt, world.getRegistryManager()); + return chunk; + } + + @ModifyExpressionValue( + at = @At( + value = "NEW", + target = "net/minecraft/world/chunk/ProtoChunk" + ), + method = "deserialize" + ) + private static ProtoChunk readProtoChunkAttachments(ProtoChunk chunk, ServerWorld world, PointOfInterestStorage poiStorage, ChunkPos chunkPos, NbtCompound nbt) { ((AttachmentTargetImpl) chunk).fabric_readAttachmentsFromNbt(nbt, world.getRegistryManager()); return chunk; } @@ -52,8 +64,6 @@ private static WorldChunk readChunkAttachments(WorldChunk chunk, ServerWorld wor method = "serialize" ) private static void writeChunkAttachments(ServerWorld world, Chunk chunk, CallbackInfoReturnable cir) { - if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.LEVELCHUNK) { - ((AttachmentTargetImpl) chunk).fabric_writeAttachmentsToNbt(cir.getReturnValue(), world.getRegistryManager()); - } + ((AttachmentTargetImpl) chunk).fabric_writeAttachmentsToNbt(cir.getReturnValue(), world.getRegistryManager()); } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java new file mode 100644 index 0000000000..f7c5386980 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.chunk.ProtoChunk; +import net.minecraft.world.chunk.WorldChunk; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(WorldChunk.class) +public class WorldChunkMixin { + @Inject( + method = "(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/ProtoChunk;Lnet/minecraft/world/chunk/WorldChunk$EntityLoader;)V", + at = @At("TAIL") + ) + public void transferProtoChunkAttachement(ServerWorld world, ProtoChunk protoChunk, WorldChunk.EntityLoader entityLoader, CallbackInfo ci) { + AttachmentTargetImpl.transfer(protoChunk, (AttachmentTarget) this, false); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java new file mode 100644 index 0000000000..6412fe2d53 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment; + +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.WrapperProtoChunk; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(WrapperProtoChunk.class) +public class WrapperProtoChunkMixin implements AttachmentTargetImpl { + @Shadow + @Final + private WorldChunk wrapped; + + @Override + @Nullable + public T getAttached(AttachmentType type) { + return this.wrapped.getAttached(type); + } + + @Override + @Nullable + public T setAttached(AttachmentType type, @Nullable T value) { + return this.wrapped.setAttached(type, value); + } + + @Override + public boolean hasAttached(AttachmentType type) { + return this.wrapped.hasAttached(type); + } + + @Override + public void fabric_writeAttachmentsToNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapperLookup) { + ((AttachmentTargetImpl) this.wrapped).fabric_writeAttachmentsToNbt(nbt, wrapperLookup); + } + + @Override + public void fabric_readAttachmentsFromNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapperLookup) { + ((AttachmentTargetImpl) this.wrapped).fabric_readAttachmentsFromNbt(nbt, wrapperLookup); + } + + @Override + public boolean fabric_hasPersistentAttachments() { + return ((AttachmentTargetImpl) this.wrapped).fabric_hasPersistentAttachments(); + } + + @Override + public Map, ?> fabric_getAttachments() { + return ((AttachmentTargetImpl) this.wrapped).fabric_getAttachments(); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json index 1ce1c066a8..57e55caff1 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json @@ -8,7 +8,9 @@ "BlockEntityUpdateS2CPacketMixin", "ChunkSerializerMixin", "EntityMixin", - "ServerWorldMixin" + "ServerWorldMixin", + "WorldChunkMixin", + "WrapperProtoChunkMixin" ], "injectors": { "defaultRequire": 1 diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json index 9b6ae5f581..ee82f417c4 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json @@ -37,7 +37,7 @@ "fabric-api:module-lifecycle": "experimental", "loom:injected_interfaces": { "net/minecraft/class_2586": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], - "net/minecraft/class_2818": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], + "net/minecraft/class_2791": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], "net/minecraft/class_1297": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], "net/minecraft/class_3218": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"] } diff --git a/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java index 3c72ee3a25..5ba1a46f6b 100644 --- a/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java +++ b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java @@ -48,6 +48,7 @@ import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; import net.minecraft.util.math.BlockPos; +import net.minecraft.world.chunk.ProtoChunk; import net.minecraft.world.chunk.WorldChunk; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; @@ -82,8 +83,9 @@ void testTargets() { Entity entity = mock(Entity.class, CALLS_REAL_METHODS); BlockEntity blockEntity = mock(BlockEntity.class, CALLS_REAL_METHODS); WorldChunk worldChunk = mock(WorldChunk.class, CALLS_REAL_METHODS); + ProtoChunk protoChunk = mock(ProtoChunk.class, CALLS_REAL_METHODS); - for (AttachmentTarget target : new AttachmentTarget[]{serverWorld, entity, blockEntity, worldChunk}) { + for (AttachmentTarget target : new AttachmentTarget[]{serverWorld, entity, blockEntity, worldChunk, protoChunk}) { testForTarget(target, basic); } } @@ -161,8 +163,8 @@ void testEntityCopy() { Entity respawnTarget = mock(Entity.class, CALLS_REAL_METHODS); Entity nonRespawnTarget = mock(Entity.class, CALLS_REAL_METHODS); - AttachmentTargetImpl.copyOnRespawn(original, respawnTarget, true); - AttachmentTargetImpl.copyOnRespawn(original, nonRespawnTarget, false); + AttachmentTargetImpl.transfer(original, respawnTarget, true); + AttachmentTargetImpl.transfer(original, nonRespawnTarget, false); assertTrue(respawnTarget.hasAttached(copiedOnRespawn)); assertFalse(respawnTarget.hasAttached(notCopiedOnRespawn)); assertTrue(nonRespawnTarget.hasAttached(copiedOnRespawn)); diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java index 5c7e4dbbf8..dad16a7e21 100644 --- a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java @@ -20,13 +20,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.ProtoChunk; import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.WrapperProtoChunk; +import net.minecraft.world.gen.GenerationStep; +import net.minecraft.world.gen.feature.DefaultFeatureConfig; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.biome.v1.BiomeModifications; +import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; public class AttachmentTestMod implements ModInitializer { @@ -36,33 +50,78 @@ public class AttachmentTestMod implements ModInitializer { new Identifier(MOD_ID, "persistent"), Codec.STRING ); + public static final AttachmentType FEATURE_ATTACHMENT = AttachmentRegistry.create( + new Identifier(MOD_ID, "feature") + ); + + public static final ChunkPos FAR_CHUNK_POS = new ChunkPos(30, 0); private boolean firstLaunch = true; + public static boolean featurePlaced = false; @Override public void onInitialize() { + Registry.register(Registries.FEATURE, new Identifier(MOD_ID, "set_attachment"), new SetAttachmentFeature(DefaultFeatureConfig.CODEC)); + + BiomeModifications.addFeature( + BiomeSelectors.foundInOverworld(), + GenerationStep.Feature.VEGETAL_DECORATION, + RegistryKey.of(RegistryKeys.PLACED_FEATURE, new Identifier(MOD_ID, "set_attachment")) + ); + ServerLifecycleEvents.SERVER_STARTED.register(server -> { ServerWorld overworld; WorldChunk chunk; + overworld = server.getOverworld(); + chunk = overworld.getChunk(0, 0); + if (firstLaunch) { - LOGGER.info("First launch, setting up"); + LOGGER.info("First launch, testing attachment by feature"); + + if (featurePlaced) { + if (!"feature".equals(chunk.getAttached(FEATURE_ATTACHMENT))) { + throw new AssertionError("Feature did not write attachment to ProtoChunk"); + } + } else { + LOGGER.warn("Feature not placed, could not test writing during worldgen"); + } + + LOGGER.info("setting up persistent attachments"); - overworld = server.getOverworld(); overworld.setAttached(PERSISTENT, "world_data"); - chunk = overworld.getChunk(0, 0); chunk.setAttached(PERSISTENT, "chunk_data"); + + ProtoChunk protoChunk = (ProtoChunk) overworld.getChunkManager().getChunk(FAR_CHUNK_POS.x, FAR_CHUNK_POS.z, ChunkStatus.STRUCTURE_STARTS, true); + protoChunk.setAttached(PERSISTENT, "protochunk_data"); } else { - LOGGER.info("Second launch, testing"); + LOGGER.info("Second launch, testing persistent attachments"); + + if (!"world_data".equals(overworld.getAttached(PERSISTENT))) throw new AssertionError("World attachement did not persist"); + if (!"chunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError("WorldChunk attachement did not persist"); + + WrapperProtoChunk wrapperProtoChunk = (WrapperProtoChunk) overworld.getChunkManager().getChunk(0, 0, ChunkStatus.EMPTY, true); + if (!"chunk_data".equals(wrapperProtoChunk.getAttached(PERSISTENT))) throw new AssertionError("Attachement is not accessible through WrapperProtoChunk"); - overworld = server.getOverworld(); - if (!"world_data".equals(overworld.getAttached(PERSISTENT))) throw new AssertionError(); + Chunk farChunk = overworld.getChunkManager().getChunk(FAR_CHUNK_POS.x, FAR_CHUNK_POS.z, ChunkStatus.EMPTY, true); - chunk = overworld.getChunk(0, 0); - if (!"chunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError(); + if (farChunk instanceof WrapperProtoChunk) { + LOGGER.warn("Far chunk alread generated, can't test persistence in ProtoChunk."); + } else { + if (!"protochunk_data".equals(farChunk.getAttached(PERSISTENT))) throw new AssertionError("ProtoChunk attachement did not persist"); + } } }); ServerLifecycleEvents.SERVER_STOPPING.register(server -> firstLaunch = false); + + // Testing hint: load far chunk by running /tp @s 480 ~ 0 + ServerChunkEvents.CHUNK_LOAD.register(((world, chunk) -> { + if (!chunk.getPos().equals(FAR_CHUNK_POS)) return; + + LOGGER.info("Loaded chunk {}, testing transfer of attachments to WorldChunk", FAR_CHUNK_POS); + + if (!"protochunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError("ProtoChunk attachement was not transfered to WorldChunk"); + })); } } diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/SetAttachmentFeature.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/SetAttachmentFeature.java new file mode 100644 index 0000000000..a6a588df03 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/SetAttachmentFeature.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.attachment; + +import com.mojang.serialization.Codec; + +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ProtoChunk; +import net.minecraft.world.chunk.WrapperProtoChunk; +import net.minecraft.world.gen.feature.DefaultFeatureConfig; +import net.minecraft.world.gen.feature.Feature; +import net.minecraft.world.gen.feature.util.FeatureContext; + +public class SetAttachmentFeature extends Feature { + public SetAttachmentFeature(Codec codec) { + super(codec); + } + + @Override + public boolean generate(FeatureContext context) { + Chunk chunk = context.getWorld().getChunk(context.getOrigin()); + + if (chunk.getPos().equals(new ChunkPos(0, 0))) { + AttachmentTestMod.featurePlaced = true; + + if (!(chunk instanceof ProtoChunk) || chunk instanceof WrapperProtoChunk) { + AttachmentTestMod.LOGGER.warn("Feature not attaching to ProtoChunk"); + } + + chunk.setAttached(AttachmentTestMod.FEATURE_ATTACHMENT, "feature"); + } + + return true; + } +} diff --git a/fabric-data-attachment-api-v1/src/testmod/resources/data/fabric-data-attachment-api-v1-testmod/worldgen/placed_feature/set_attachment.json b/fabric-data-attachment-api-v1/src/testmod/resources/data/fabric-data-attachment-api-v1-testmod/worldgen/placed_feature/set_attachment.json new file mode 100644 index 0000000000..538bf07310 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/resources/data/fabric-data-attachment-api-v1-testmod/worldgen/placed_feature/set_attachment.json @@ -0,0 +1,7 @@ +{ + "feature": { + "type": "fabric-data-attachment-api-v1-testmod:set_attachment", + "config": {} + }, + "placement": [] +} diff --git a/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json index 9c369f4f87..28224b760b 100644 --- a/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json @@ -7,7 +7,8 @@ "license": "Apache-2.0", "depends": { "fabric-data-attachment-api-v1": "*", - "fabric-lifecycle-events-v1": "*" + "fabric-lifecycle-events-v1": "*", + "fabric-biome-api-v1": "*" }, "entrypoints": { "main": [ diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerLifecycleEvents.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerLifecycleEvents.java index abd971e850..769d8e9b96 100644 --- a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerLifecycleEvents.java +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerLifecycleEvents.java @@ -107,6 +107,24 @@ private ServerLifecycleEvents() { } }); + /** + * Called before a Minecraft server begins saving data. + */ + public static final Event BEFORE_SAVE = EventFactory.createArrayBacked(BeforeSave.class, callbacks -> (server, flush, force) -> { + for (BeforeSave callback : callbacks) { + callback.onBeforeSave(server, flush, force); + } + }); + + /** + * Called after a Minecraft server finishes saving data. + */ + public static final Event AFTER_SAVE = EventFactory.createArrayBacked(AfterSave.class, callbacks -> (server, flush, force) -> { + for (AfterSave callback : callbacks) { + callback.onAfterSave(server, flush, force); + } + }); + @FunctionalInterface public interface ServerStarting { void onServerStarting(MinecraftServer server); @@ -160,4 +178,28 @@ public interface EndDataPackReload { */ void endDataPackReload(MinecraftServer server, LifecycledResourceManager resourceManager, boolean success); } + + @FunctionalInterface + public interface BeforeSave { + /** + * Called before a Minecraft server begins saving data. + * + * @param server the server + * @param flush is true when all chunks are being written to disk, server will likely freeze during this time + * @param force whether servers that have save-off set should save + */ + void onBeforeSave(MinecraftServer server, boolean flush, boolean force); + } + + @FunctionalInterface + public interface AfterSave { + /** + * Called before a Minecraft server begins saving data. + * + * @param server the server + * @param flush is true when all chunks are being written to disk, server will likely freeze during this time + * @param force whether servers that have save-off set should save + */ + void onAfterSave(MinecraftServer server, boolean flush, boolean force); + } } diff --git a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/MinecraftServerMixin.java b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/MinecraftServerMixin.java index 04cffbe7e3..2dcbce66e3 100644 --- a/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/MinecraftServerMixin.java +++ b/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/mixin/event/lifecycle/MinecraftServerMixin.java @@ -101,4 +101,14 @@ private void endResourceReload(Collection collection, CallbackInfoReturn return value; }, (MinecraftServer) (Object) this); } + + @Inject(method = "save", at = @At("HEAD")) + private void startSave(boolean suppressLogs, boolean flush, boolean force, CallbackInfoReturnable cir) { + ServerLifecycleEvents.BEFORE_SAVE.invoker().onBeforeSave((MinecraftServer) (Object) this, flush, force); + } + + @Inject(method = "save", at = @At("TAIL")) + private void endSave(boolean suppressLogs, boolean flush, boolean force, CallbackInfoReturnable cir) { + ServerLifecycleEvents.AFTER_SAVE.invoker().onAfterSave((MinecraftServer) (Object) this, flush, force); + } } diff --git a/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerLifecycleTests.java b/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerLifecycleTests.java index 3294bd2548..ea1e236fc8 100644 --- a/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerLifecycleTests.java +++ b/fabric-lifecycle-events-v1/src/testmod/java/net/fabricmc/fabric/test/event/lifecycle/ServerLifecycleTests.java @@ -54,5 +54,13 @@ public void onInitialize() { ServerLifecycleEvents.SYNC_DATA_PACK_CONTENTS.register((player, joined) -> { LOGGER.info("SyncDataPackContents received for {}", joined ? "join" : "reload"); }); + + ServerLifecycleEvents.BEFORE_SAVE.register((server, flush, force) -> { + LOGGER.info("Starting Save with settings: Flush:{} Force:{}", flush, force); + }); + + ServerLifecycleEvents.AFTER_SAVE.register((server, flush, force) -> { + LOGGER.info("Save Finished with settings: Flush:{} Force:{}", flush, force); + }); } } diff --git a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/api/client/render/fluid/v1/FluidRenderHandlerRegistry.java b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/api/client/render/fluid/v1/FluidRenderHandlerRegistry.java index 49ead64267..6394d9934d 100644 --- a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/api/client/render/fluid/v1/FluidRenderHandlerRegistry.java +++ b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/api/client/render/fluid/v1/FluidRenderHandlerRegistry.java @@ -16,6 +16,8 @@ package net.fabricmc.fabric.api.client.render.fluid.v1; +import org.jetbrains.annotations.Nullable; + import net.minecraft.block.Block; import net.minecraft.block.LeavesBlock; import net.minecraft.block.TransparentBlock; @@ -39,8 +41,19 @@ public interface FluidRenderHandlerRegistry { * @param fluid The Fluid. * @return The FluidRenderHandler. */ + @Nullable FluidRenderHandler get(Fluid fluid); + /** + * Get a {@link FluidRenderHandler} for a given Fluid, if it is not the + * default implementation. Supports vanilla and Fabric fluids. + * + * @param fluid The Fluid. + * @return The FluidRenderHandler. + */ + @Nullable + FluidRenderHandler getOverride(Fluid fluid); + /** * Register a {@link FluidRenderHandler} for a given Fluid. * diff --git a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/fluid/FluidRenderHandlerRegistryImpl.java b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/fluid/FluidRenderHandlerRegistryImpl.java index d2715db977..86737d1b45 100644 --- a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/fluid/FluidRenderHandlerRegistryImpl.java +++ b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/fluid/FluidRenderHandlerRegistryImpl.java @@ -19,6 +19,8 @@ import java.util.IdentityHashMap; import java.util.Map; +import org.jetbrains.annotations.Nullable; + import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.LeavesBlock; @@ -55,10 +57,13 @@ public FluidRenderHandlerRegistryImpl() { } @Override + @Nullable public FluidRenderHandler get(Fluid fluid) { return handlers.get(fluid); } + @Override + @Nullable public FluidRenderHandler getOverride(Fluid fluid) { return modHandlers.get(fluid); } @@ -107,10 +112,10 @@ public Sprite[] getFluidSprites(BlockRenderView view, BlockPos pos, FluidState s } }; - register(Fluids.WATER, waterHandler); - register(Fluids.FLOWING_WATER, waterHandler); - register(Fluids.LAVA, lavaHandler); - register(Fluids.FLOWING_LAVA, lavaHandler); + handlers.put(Fluids.WATER, waterHandler); + handlers.put(Fluids.FLOWING_WATER, waterHandler); + handlers.put(Fluids.LAVA, lavaHandler); + handlers.put(Fluids.FLOWING_LAVA, lavaHandler); handlers.putAll(modHandlers); SpriteAtlasTexture texture = MinecraftClient.getInstance() diff --git a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/fluid/FluidRendererMixin.java b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/fluid/FluidRendererMixin.java index 5ec9625e36..81735984e1 100644 --- a/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/fluid/FluidRendererMixin.java +++ b/fabric-rendering-fluids-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/fluid/FluidRendererMixin.java @@ -85,7 +85,7 @@ public void tesselate(BlockRenderView view, BlockPos pos, VertexConsumer vertexC @Unique private void tessellateViaHandler(BlockRenderView view, BlockPos pos, VertexConsumer vertexConsumer, BlockState blockState, FluidState fluidState, CallbackInfo info) { FluidRendererHookContainer ctr = fabric_renderHandler.get(); - FluidRenderHandler handler = ((FluidRenderHandlerRegistryImpl) FluidRenderHandlerRegistry.INSTANCE).getOverride(fluidState.getFluid()); + FluidRenderHandler handler = FluidRenderHandlerRegistry.INSTANCE.get(fluidState.getFluid()); ctr.view = view; ctr.pos = pos; diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/ColorResolverRegistry.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/ColorResolverRegistry.java new file mode 100644 index 0000000000..4bdddc86ec --- /dev/null +++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/ColorResolverRegistry.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.client.rendering.v1; + +import java.util.Set; + +import org.jetbrains.annotations.UnmodifiableView; + +import net.minecraft.client.color.world.BiomeColors; +import net.minecraft.world.BlockRenderView; +import net.minecraft.world.biome.ColorResolver; + +import net.fabricmc.fabric.impl.client.rendering.ColorResolverRegistryImpl; + +/** + * The registry for custom {@link ColorResolver}s. Custom resolvers must be registered during client initialization for + * them to be usable in {@link BlockRenderView#getColor}. Calling this method may throw an exception if the passed + * resolver is not registered with this class. Vanilla resolvers found in {@link BiomeColors} are automatically + * registered. + * + *

Other mods may also require custom resolvers to be registered if they provide additional functionality related to + * color resolvers. + */ +public final class ColorResolverRegistry { + private ColorResolverRegistry() { + } + + /** + * Registers a custom {@link ColorResolver} for use in {@link BlockRenderView#getColor}. This method should be + * called during client initialization. + * + * @param resolver the resolver to register + */ + public static void register(ColorResolver resolver) { + ColorResolverRegistryImpl.register(resolver); + } + + /** + * Gets a view of all registered {@link ColorResolver}s, including all vanilla resolvers. + * + * @return a view of all registered resolvers + */ + @UnmodifiableView + public static Set getAllResolvers() { + return ColorResolverRegistryImpl.getAllResolvers(); + } + + /** + * Gets a view of all registered {@link ColorResolver}s, not including vanilla resolvers. + * + * @return a view of all registered custom resolvers + */ + @UnmodifiableView + public static Set getCustomResolvers() { + return ColorResolverRegistryImpl.getCustomResolvers(); + } + + /** + * Checks whether the given {@link ColorResolver} is registered. Vanilla resolvers are always registered. + * + * @param resolver the resolver + * @return whether the given resolver is registered + */ + public static boolean isRegistered(ColorResolver resolver) { + return getAllResolvers().contains(resolver); + } +} diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/ColorResolverRegistryImpl.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/ColorResolverRegistryImpl.java new file mode 100644 index 0000000000..e49c231d68 --- /dev/null +++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/ColorResolverRegistryImpl.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.client.rendering; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import org.jetbrains.annotations.UnmodifiableView; + +import net.minecraft.client.color.world.BiomeColors; +import net.minecraft.client.world.BiomeColorCache; +import net.minecraft.world.biome.ColorResolver; + +public final class ColorResolverRegistryImpl { + // Includes vanilla resolvers + private static final Set ALL_RESOLVERS = new HashSet<>(); + // Does not include vanilla resolvers + private static final Set CUSTOM_RESOLVERS = new HashSet<>(); + private static final Set ALL_RESOLVERS_VIEW = Collections.unmodifiableSet(ALL_RESOLVERS); + private static final Set CUSTOM_RESOLVERS_VIEW = Collections.unmodifiableSet(CUSTOM_RESOLVERS); + + static { + ALL_RESOLVERS.add(BiomeColors.GRASS_COLOR); + ALL_RESOLVERS.add(BiomeColors.FOLIAGE_COLOR); + ALL_RESOLVERS.add(BiomeColors.WATER_COLOR); + } + + private ColorResolverRegistryImpl() { + } + + public static void register(ColorResolver resolver) { + ALL_RESOLVERS.add(resolver); + CUSTOM_RESOLVERS.add(resolver); + } + + @UnmodifiableView + public static Set getAllResolvers() { + return ALL_RESOLVERS_VIEW; + } + + @UnmodifiableView + public static Set getCustomResolvers() { + return CUSTOM_RESOLVERS_VIEW; + } + + public static Reference2ReferenceMap createCustomCacheMap(Function cacheFactory) { + Reference2ReferenceOpenHashMap map = new Reference2ReferenceOpenHashMap<>(); + + for (ColorResolver resolver : CUSTOM_RESOLVERS) { + map.put(resolver, cacheFactory.apply(resolver)); + } + + map.trim(); + return map; + } +} diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/ClientWorldMixin.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/ClientWorldMixin.java new file mode 100644 index 0000000000..8249e6c3f5 --- /dev/null +++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/ClientWorldMixin.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.rendering; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.world.BiomeColorCache; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.biome.ColorResolver; + +import net.fabricmc.fabric.impl.client.rendering.ColorResolverRegistryImpl; + +@Mixin(ClientWorld.class) +public abstract class ClientWorldMixin { + // Do not use the vanilla map because it is an Object2ObjectArrayMap. Array maps have O(n) retrievals compared to + // hash maps' O(1) retrievals. If many custom ColorResolvers are registered, this may have a non-negligible + // performance impact. + @Unique + private final Reference2ReferenceMap customColorCache = ColorResolverRegistryImpl.createCustomCacheMap(resolver -> new BiomeColorCache(pos -> calculateColor(pos, resolver))); + + @Shadow + public abstract int calculateColor(BlockPos pos, ColorResolver colorResolver); + + @Inject(method = "resetChunkColor(Lnet/minecraft/util/math/ChunkPos;)V", at = @At("RETURN")) + private void onResetChunkColor(ChunkPos chunkPos, CallbackInfo ci) { + for (BiomeColorCache cache : customColorCache.values()) { + cache.reset(chunkPos.x, chunkPos.z); + } + } + + @Inject(method = "reloadColor()V", at = @At("RETURN")) + private void onReloadColor(CallbackInfo ci) { + for (BiomeColorCache cache : customColorCache.values()) { + cache.reset(); + } + } + + @ModifyExpressionValue(method = "getColor(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/biome/ColorResolver;)I", at = @At(value = "INVOKE", target = "it/unimi/dsi/fastutil/objects/Object2ObjectArrayMap.get(Ljava/lang/Object;)Ljava/lang/Object;")) + private Object modifyNullCache(/* BiomeColorCache */ Object cache, BlockPos pos, ColorResolver resolver) { + if (cache == null) { + cache = customColorCache.get(resolver); + + if (cache == null) { + throw new UnsupportedOperationException("ClientWorld.getColor called with unregistered ColorResolver " + resolver); + } + } + + return cache; + } +} diff --git a/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json b/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json index fd802ffc65..0ec5218d1c 100644 --- a/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json +++ b/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json @@ -9,6 +9,7 @@ "BlockEntityRendererFactoriesMixin", "BuiltinModelItemRendererMixin", "CapeFeatureRendererMixin", + "ClientWorldMixin", "DimensionEffectsAccessor", "EntityModelLayersAccessor", "EntityModelsMixin", diff --git a/fabric-rendering-v1/src/testmod/java/net/fabricmc/fabric/test/rendering/CustomColorResolverTestInit.java b/fabric-rendering-v1/src/testmod/java/net/fabricmc/fabric/test/rendering/CustomColorResolverTestInit.java new file mode 100644 index 0000000000..926740eed3 --- /dev/null +++ b/fabric-rendering-v1/src/testmod/java/net/fabricmc/fabric/test/rendering/CustomColorResolverTestInit.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.rendering; + +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.Block; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; + +import net.fabricmc.api.ModInitializer; + +public class CustomColorResolverTestInit implements ModInitializer { + public static final Block CUSTOM_COLOR_BLOCK = new Block(AbstractBlock.Settings.create()); + public static final Item CUSTOM_COLOR_BLOCK_ITEM = new BlockItem(CUSTOM_COLOR_BLOCK, new Item.Settings()); + + @Override + public void onInitialize() { + Registry.register(Registries.BLOCK, new Identifier("fabric-rendering-v1-testmod", "custom_color_block"), CUSTOM_COLOR_BLOCK); + Registry.register(Registries.ITEM, new Identifier("fabric-rendering-v1-testmod", "custom_color_block"), CUSTOM_COLOR_BLOCK_ITEM); + } +} diff --git a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json index 3c5f3fe0a6..57107b39f9 100644 --- a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json @@ -8,11 +8,13 @@ "entrypoints": { "main": [ "net.fabricmc.fabric.test.rendering.CustomAtlasSourcesTestInit", + "net.fabricmc.fabric.test.rendering.CustomColorResolverTestInit", "net.fabricmc.fabric.test.rendering.TooltipComponentTestInit" ], "client": [ "net.fabricmc.fabric.test.rendering.client.ArmorRenderingTests", "net.fabricmc.fabric.test.rendering.client.CustomAtlasSourcesTest", + "net.fabricmc.fabric.test.rendering.client.CustomColorResolverTest", "net.fabricmc.fabric.test.rendering.client.DimensionalRenderingTest", "net.fabricmc.fabric.test.rendering.client.FeatureRendererTest", "net.fabricmc.fabric.test.rendering.client.HudAndShaderTest", diff --git a/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/CustomColorResolverTest.java b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/CustomColorResolverTest.java new file mode 100644 index 0000000000..3d7cd88b10 --- /dev/null +++ b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/CustomColorResolverTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.rendering.client; + +import net.minecraft.world.biome.ColorResolver; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.ColorResolverRegistry; +import net.fabricmc.fabric.test.rendering.CustomColorResolverTestInit; + +public class CustomColorResolverTest implements ClientModInitializer { + public static final ColorResolver TEST_COLOR_RESOLVER = (biome, x, z) -> { + if (biome.hasPrecipitation()) { + return 0xFFFF00FF; + } else { + return 0xFFFFFF00; + } + }; + + @Override + public void onInitializeClient() { + ColorResolverRegistry.register(TEST_COLOR_RESOLVER); + + ColorProviderRegistry.BLOCK.register((state, world, pos, tintIndex) -> { + if (world != null && pos != null) { + return world.getColor(pos, TEST_COLOR_RESOLVER); + } else { + return -1; + } + }, CustomColorResolverTestInit.CUSTOM_COLOR_BLOCK); + } +} diff --git a/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/blockstates/custom_color_block.json b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/blockstates/custom_color_block.json new file mode 100644 index 0000000000..2b12403da0 --- /dev/null +++ b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/blockstates/custom_color_block.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "fabric-rendering-v1-testmod:block/custom_color_block" } + } +} diff --git a/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/models/block/custom_color_block.json b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/models/block/custom_color_block.json new file mode 100644 index 0000000000..62703835e6 --- /dev/null +++ b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/models/block/custom_color_block.json @@ -0,0 +1,20 @@ +{ + "parent": "block/block", + "textures": { + "all": "fabric-rendering-v1-testmod:block/blank", + "particle": "#all" + }, + "elements": [ + { "from": [ 0, 0, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "cullface": "down" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "up" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "cullface": "north" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "cullface": "south" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "cullface": "west" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "cullface": "east" } + } + } + ] +} diff --git a/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/textures/block/blank.png b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/textures/block/blank.png new file mode 100644 index 0000000000..3869a61f64 Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/assets/fabric-rendering-v1-testmod/textures/block/blank.png differ