From eb0cfc4ea46876e9f771b67b2c8231831af02ff9 Mon Sep 17 00:00:00 2001 From: aromaa Date: Sun, 10 Nov 2024 14:21:28 +0200 Subject: [PATCH 01/24] Fix registries that rely on order Vanilla no longer allows to register items to arbitrary ids but some of our registries rely on this due to the way vanilla stores this data and has no natural registries for them. Fixing this by registering the items in the order we want them to be present in and then verying their ids match the ones we expected afterwards. While this is very brittle, it is the most straight forward way to ensure we stay in sync with vanilla elements without more complex changes. --- .../common/registry/RegistryHolderLogic.java | 23 ++++++++----------- .../common/registry/RegistryLoader.java | 3 ++- .../loader/VanillaRegistryLoader.java | 6 ++--- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java b/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java index cca757cd88c..ee5b05dd2c7 100644 --- a/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java +++ b/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java @@ -180,19 +180,16 @@ public Registry createRegistry(final RegistryType type, final @Nullabl if (defaultValues != null) { final MappedRegistry mr = (MappedRegistry) registry; defaultValues.forEach((vk, vi, vv) -> { - if (vi.isPresent()) { - mr.register( - net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), - vv, - RegistrationInfo.BUILT_IN - ); - } else { - mr.register( - net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), - vv, - RegistrationInfo.BUILT_IN - ); - } + mr.register( + net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), + vv, + RegistrationInfo.BUILT_IN + ); + vi.ifPresent(id -> { + if (mr.getId(vv) != id) { + throw new IllegalStateException("Registry entry " + vk + " was expected to have id of " + id + " but was instead " + mr.getId(vv)); + } + }); }); } diff --git a/src/main/java/org/spongepowered/common/registry/RegistryLoader.java b/src/main/java/org/spongepowered/common/registry/RegistryLoader.java index b16aaa5d37b..8521b091e51 100644 --- a/src/main/java/org/spongepowered/common/registry/RegistryLoader.java +++ b/src/main/java/org/spongepowered/common/registry/RegistryLoader.java @@ -30,6 +30,7 @@ import org.spongepowered.api.registry.RegistryKey; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -37,7 +38,7 @@ public final class RegistryLoader extends InitialRegistryData { - private final Map values = new HashMap<>(); + private final Map values = new LinkedHashMap<>(); private @MonotonicNonNull Map ids; private RegistryLoader() { diff --git a/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java b/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java index 8dd24c87c8d..f164896ebab 100644 --- a/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java +++ b/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java @@ -239,11 +239,11 @@ private static RegistryLoader criterion() { private static RegistryLoader fireworkShape() { return RegistryLoader.of(l -> { - l.addWithId(FireworkExplosion.Shape.BURST.getId(), FireworkShapes.BURST, () -> (FireworkShape) (Object) FireworkExplosion.Shape.BURST); - l.addWithId(FireworkExplosion.Shape.CREEPER.getId(), FireworkShapes.CREEPER, () -> (FireworkShape) (Object) FireworkExplosion.Shape.CREEPER); - l.addWithId(FireworkExplosion.Shape.LARGE_BALL.getId(), FireworkShapes.LARGE_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.LARGE_BALL); l.addWithId(FireworkExplosion.Shape.SMALL_BALL.getId(), FireworkShapes.SMALL_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.SMALL_BALL); + l.addWithId(FireworkExplosion.Shape.LARGE_BALL.getId(), FireworkShapes.LARGE_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.LARGE_BALL); l.addWithId(FireworkExplosion.Shape.STAR.getId(), FireworkShapes.STAR, () -> (FireworkShape) (Object) FireworkExplosion.Shape.STAR); + l.addWithId(FireworkExplosion.Shape.CREEPER.getId(), FireworkShapes.CREEPER, () -> (FireworkShape) (Object) FireworkExplosion.Shape.CREEPER); + l.addWithId(FireworkExplosion.Shape.BURST.getId(), FireworkShapes.BURST, () -> (FireworkShape) (Object) FireworkExplosion.Shape.BURST); }); } From 6a88d5462a9d8ae289b432e6fc3926381c6d82d3 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 11 Nov 2024 21:34:34 +0200 Subject: [PATCH 02/24] Fix Chunk#entities returning all entities of the world --- .../common/world/volume/VolumeStreamUtils.java | 4 ++-- .../world/level/chunk/LevelChunkMixin_API.java | 14 +++++++++----- .../entityactivation/EntityActivationRange.java | 3 +-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java b/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java index 60b6d5a4af7..791286b2ae7 100644 --- a/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java +++ b/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java @@ -218,14 +218,14 @@ public static void validateStreamArgs(final Vector3i min, final Vector3i max, fi ) { if (chunk.getLevel() instanceof ServerLevel) { return ((PersistentEntitySectionManagerAccessor) ((ServerLevelAccessor) chunk.getLevel()).accessor$getEntityManager()).accessor$sectionStorage() - .getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()) + .getExistingSectionsInChunk(chunk.getPos().toLong()) .flatMap(EntitySection::getEntities) .filter(entity -> VecHelper.inBounds(entity.blockPosition(), min, max)) .map(entity -> new AbstractMap.SimpleEntry<>(entity.blockPosition(), entity)); } else if (Sponge.isClientAvailable() && chunk.getLevel() instanceof ClientLevel) { return ((TransientEntitySectionManagerAccessor) ((ClientLevelAccessor) chunk.getLevel()).accessor$getEntityStorage()) .accessor$sectionStorage() - .getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()) + .getExistingSectionsInChunk(chunk.getPos().toLong()) .flatMap(EntitySection::getEntities) .filter(entity -> VecHelper.inBounds(entity.blockPosition(), min, max)) .map(entity -> new AbstractMap.SimpleEntry<>(entity.blockPosition(), entity)); diff --git a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java index eb2f28f511c..4ef62fcb195 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java +++ b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java @@ -39,6 +39,9 @@ import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.blending.BlendingData; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -71,7 +74,9 @@ import org.spongepowered.asm.mixin.Intrinsic; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.common.accessor.server.level.ServerLevelAccessor; import org.spongepowered.common.accessor.world.level.LevelAccessor; +import org.spongepowered.common.accessor.world.level.entity.PersistentEntitySectionManagerAccessor; import org.spongepowered.common.bridge.world.level.LevelBridge; import org.spongepowered.common.bridge.world.level.chunk.LevelChunkBridge; import org.spongepowered.common.data.holder.SpongeServerLocationBaseDataHolder; @@ -100,7 +105,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; @Mixin(net.minecraft.world.level.chunk.LevelChunk.class) @Implements(@Interface(iface = WorldChunk.class, prefix = "worldChunk$", remap = Interface.Remap.NONE)) @@ -418,12 +422,12 @@ public Optional entity(final UUID uuid) { .filter(x -> x.chunkPosition().equals(this.chunkPos)); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Collection entities() { - return (Collection) (Object) StreamSupport.stream( - ((LevelAccessor) this.level).invoker$getEntities().getAll().spliterator(), false) - .collect(Collectors.toList()); + final PersistentEntitySectionManager entityManager = ((ServerLevelAccessor) this.level).accessor$getEntityManager(); + final EntitySectionStorage entitySectionStorage = ((PersistentEntitySectionManagerAccessor) entityManager).accessor$sectionStorage(); + return (Collection) entitySectionStorage.getExistingSectionsInChunk(this.chunkPos.toLong()).flatMap(EntitySection::getEntities).toList(); } @SuppressWarnings({"rawtypes", "unchecked"}) diff --git a/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java b/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java index 0e1337201d6..6f123326cde 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java +++ b/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java @@ -25,7 +25,6 @@ package org.spongepowered.common.mixin.plugin.entityactivation; import com.google.common.collect.ImmutableMap; -import net.minecraft.core.SectionPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -262,7 +261,7 @@ public static void activateEntities(final ServerLevel world) { private static void activateChunkEntities(final ServerPlayer player, final LevelChunk chunk) { final PersistentEntitySectionManager entityManager = ((ServerLevelAccessor) chunk.getLevel()).accessor$getEntityManager(); final EntitySectionStorage entitySectionStorage = ((PersistentEntitySectionManagerAccessor) entityManager).accessor$sectionStorage(); - entitySectionStorage.getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()).flatMap(EntitySection::getEntities).forEach(entity -> { + entitySectionStorage.getExistingSectionsInChunk(chunk.getPos().toLong()).flatMap(EntitySection::getEntities).forEach(entity -> { if (!entity.chunkPosition().equals(chunk.getPos())) { return; } From df343386f5e63fc821c54b9c2aec7ef12de4d392 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 11 Nov 2024 21:35:37 +0200 Subject: [PATCH 03/24] Fix ChunkEvent#Unload fired too late --- .../core/server/level/ChunkHolderMixin.java | 34 ++++++++++++++++--- .../core/server/level/ChunkMapMixin.java | 14 -------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java index a58cb5b70d4..6b3535cd498 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java @@ -53,18 +53,44 @@ abstract class ChunkHolderMixin extends GenerationChunkHolderMixin { @Shadow public abstract CompletableFuture> shadow$getEntityTickingChunkFuture(); // @formatter:on + private LevelChunk impl$loadedChunk; + /** * After onFullChunkStatusChange */ @Inject(method = "lambda$scheduleFullChunkPromotion$4", at = @At("TAIL")) private void impl$onScheduleFullChunkPromotion(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { - if ($$1x == FullChunkStatus.ENTITY_TICKING && ShouldFire.CHUNK_EVENT_LOAD) { + if ($$1x == FullChunkStatus.ENTITY_TICKING) { this.shadow$getEntityTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(chunk -> { - final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); - final ChunkEvent.Load event = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(), + this.impl$loadedChunk = chunk; + if (ShouldFire.CHUNK_EVENT_LOAD) { + final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); + final ChunkEvent.Load event = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(), (WorldChunk) chunk, chunkPos, (ResourceKey) (Object) chunk.getLevel().dimension().location()); - SpongeCommon.post(event); + SpongeCommon.post(event); + } }); } } + + @Inject(method = "demoteFullChunk", at = @At("HEAD")) + private void impl$onDemoteFullChunkPre(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { + if (this.impl$loadedChunk != null && ShouldFire.CHUNK_EVENT_UNLOAD_PRE) { + final Vector3i chunkPos = VecHelper.toVector3i(this.impl$loadedChunk.getPos()); + final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPre(PhaseTracker.getInstance().currentCause(), + (WorldChunk) this.impl$loadedChunk, chunkPos, (ResourceKey) (Object) this.impl$loadedChunk.getLevel().dimension().location()); + SpongeCommon.post(event); + } + } + + @Inject(method = "demoteFullChunk", at = @At("TAIL")) + private void impl$onDemoteFullChunkPost(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { + if (this.impl$loadedChunk != null && ShouldFire.CHUNK_EVENT_UNLOAD_POST) { + final Vector3i chunkPos = VecHelper.toVector3i(this.impl$loadedChunk.getPos()); + final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPost(PhaseTracker.getInstance().currentCause(), chunkPos, + (ResourceKey) (Object) this.impl$loadedChunk.getLevel().dimension().location()); + SpongeCommon.post(event); + } + this.impl$loadedChunk = null; + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java index 2df47d7beea..d3b06d5f6c9 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java @@ -128,14 +128,6 @@ public abstract class ChunkMapMixin implements ChunkMapBridge { ) ) private void impl$onSetUnloaded(final ServerLevel level, final LevelChunk chunk) { - final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); - - if (ShouldFire.CHUNK_EVENT_UNLOAD_PRE) { - final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPre(PhaseTracker.getInstance().currentCause(), - (WorldChunk) chunk, chunkPos, (ResourceKey) (Object) this.level.dimension().location()); - SpongeCommon.post(event); - } - level.unload(chunk); for (final Direction dir : Constants.Chunk.CARDINAL_DIRECTIONS) { @@ -147,12 +139,6 @@ public abstract class ChunkMapMixin implements ChunkMapBridge { ((LevelChunkBridge) neighbor).bridge$setNeighborChunk(oppositeIndex, null); } } - - if (ShouldFire.CHUNK_EVENT_UNLOAD_POST) { - final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPost(PhaseTracker.getInstance().currentCause(), chunkPos, - (ResourceKey) (Object) this.level.dimension().location()); - SpongeCommon.post(event); - } } @Inject(method = "save", at = @At(value = "HEAD"), cancellable = true) From 772c1989b686484b06d51e3810649f6c9c91f249 Mon Sep 17 00:00:00 2001 From: Nel <57587152+nelind3@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:29:23 +0100 Subject: [PATCH 04/24] Update ServerLevelMixin::save to match the vanilla impl again (#4148) --- .../common/mixin/core/server/level/ServerLevelMixin.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java index 98c8c3e54a4..6dc23785178 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java @@ -39,6 +39,7 @@ import net.minecraft.server.players.PlayerList; import net.minecraft.util.ProgressListener; import net.minecraft.world.RandomSequences; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.player.Player; @@ -52,6 +53,7 @@ import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.end.EndDragonFight; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.storage.LevelStorageSource; @@ -128,6 +130,7 @@ public abstract class ServerLevelMixin extends LevelMixin implements ServerLevel // @formatter:off @Shadow @Final private ServerLevelData serverLevelData; + @Shadow @Final private PersistentEntitySectionManager entityManager; @Shadow @Final private LevelTicks blockTicks; @Shadow @Final private LevelTicks fluidTicks; @Shadow private int emptyTime; @@ -374,6 +377,12 @@ public void save(@Nullable final ProgressListener progress, final boolean flush, chunkProvider.save(flush); } + if (flush) { + this.entityManager.saveAll(); + } else { + this.entityManager.autoSave(); + } + Sponge.eventManager().post(SpongeEventFactory.createSaveWorldEventPost(currentCause, ((ServerWorld) this))); } } From 64bec579a52f098fda968e1c7356a5ed446b6c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossi=20Erkkil=C3=A4?= <52257907+avaruus1@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:01:25 +0200 Subject: [PATCH 05/24] Add key for item model (#4147) --- SpongeAPI | 2 +- .../common/data/provider/item/stack/ItemStackData.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/SpongeAPI b/SpongeAPI index 3e5a2ce6632..d4641e160e8 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit 3e5a2ce6632f6eeae1461630ac0149cacdf6e8db +Subproject commit d4641e160e878274d3ac3599d27fc6f583f33acd diff --git a/src/main/java/org/spongepowered/common/data/provider/item/stack/ItemStackData.java b/src/main/java/org/spongepowered/common/data/provider/item/stack/ItemStackData.java index 4b991d5a3ca..5fbc6f83717 100644 --- a/src/main/java/org/spongepowered/common/data/provider/item/stack/ItemStackData.java +++ b/src/main/java/org/spongepowered/common/data/provider/item/stack/ItemStackData.java @@ -183,6 +183,9 @@ public static void register(final DataProviderRegistrator registrator) { return true; }) .supports(h -> !h.has(DataComponents.MAX_DAMAGE)) + .create(Keys.MODEL) + .get(stack -> (ResourceKey) (Object) stack.get(DataComponents.ITEM_MODEL)) + .set((stack, model) -> stack.set(DataComponents.ITEM_MODEL, (ResourceLocation) (Object) model)) .create(Keys.ITEM_DURABILITY) .get(stack -> stack.getMaxDamage() - stack.getDamageValue()) .set((stack, durability) -> stack.setDamageValue(stack.getMaxDamage() - durability)) From 9b83c9328f0f13be96cd49772a04c8f01737093e Mon Sep 17 00:00:00 2001 From: aromaa Date: Sun, 10 Nov 2024 14:21:28 +0200 Subject: [PATCH 06/24] Fix registries that rely on order Vanilla no longer allows to register items to arbitrary ids but some of our registries rely on this due to the way vanilla stores this data and has no natural registries for them. Fixing this by registering the items in the order we want them to be present in and then verying their ids match the ones we expected afterwards. While this is very brittle, it is the most straight forward way to ensure we stay in sync with vanilla elements without more complex changes. --- .../common/registry/RegistryHolderLogic.java | 23 ++++++++----------- .../common/registry/RegistryLoader.java | 3 ++- .../loader/VanillaRegistryLoader.java | 6 ++--- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java b/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java index 5fa7a4589bd..6aa0912b050 100644 --- a/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java +++ b/src/main/java/org/spongepowered/common/registry/RegistryHolderLogic.java @@ -178,19 +178,16 @@ public Registry createRegistry(final RegistryType type, final @Nullabl if (defaultValues != null) { final MappedRegistry mr = (MappedRegistry) registry; defaultValues.forEach((vk, vi, vv) -> { - if (vi.isPresent()) { - mr.register( - net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), - vv, - RegistrationInfo.BUILT_IN - ); - } else { - mr.register( - net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), - vv, - RegistrationInfo.BUILT_IN - ); - } + mr.register( + net.minecraft.resources.ResourceKey.create(key, (ResourceLocation) (Object) vk), + vv, + RegistrationInfo.BUILT_IN + ); + vi.ifPresent(id -> { + if (mr.getId(vv) != id) { + throw new IllegalStateException("Registry entry " + vk + " was expected to have id of " + id + " but was instead " + mr.getId(vv)); + } + }); }); } diff --git a/src/main/java/org/spongepowered/common/registry/RegistryLoader.java b/src/main/java/org/spongepowered/common/registry/RegistryLoader.java index b16aaa5d37b..8521b091e51 100644 --- a/src/main/java/org/spongepowered/common/registry/RegistryLoader.java +++ b/src/main/java/org/spongepowered/common/registry/RegistryLoader.java @@ -30,6 +30,7 @@ import org.spongepowered.api.registry.RegistryKey; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -37,7 +38,7 @@ public final class RegistryLoader extends InitialRegistryData { - private final Map values = new HashMap<>(); + private final Map values = new LinkedHashMap<>(); private @MonotonicNonNull Map ids; private RegistryLoader() { diff --git a/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java b/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java index 6a04310d6d7..ab2279e8cb9 100644 --- a/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java +++ b/src/main/java/org/spongepowered/common/registry/loader/VanillaRegistryLoader.java @@ -251,11 +251,11 @@ private static RegistryLoader criterion() { private static RegistryLoader fireworkShape() { return RegistryLoader.of(l -> { - l.addWithId(FireworkExplosion.Shape.BURST.getId(), FireworkShapes.BURST, () -> (FireworkShape) (Object) FireworkExplosion.Shape.BURST); - l.addWithId(FireworkExplosion.Shape.CREEPER.getId(), FireworkShapes.CREEPER, () -> (FireworkShape) (Object) FireworkExplosion.Shape.CREEPER); - l.addWithId(FireworkExplosion.Shape.LARGE_BALL.getId(), FireworkShapes.LARGE_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.LARGE_BALL); l.addWithId(FireworkExplosion.Shape.SMALL_BALL.getId(), FireworkShapes.SMALL_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.SMALL_BALL); + l.addWithId(FireworkExplosion.Shape.LARGE_BALL.getId(), FireworkShapes.LARGE_BALL, () -> (FireworkShape) (Object) FireworkExplosion.Shape.LARGE_BALL); l.addWithId(FireworkExplosion.Shape.STAR.getId(), FireworkShapes.STAR, () -> (FireworkShape) (Object) FireworkExplosion.Shape.STAR); + l.addWithId(FireworkExplosion.Shape.CREEPER.getId(), FireworkShapes.CREEPER, () -> (FireworkShape) (Object) FireworkExplosion.Shape.CREEPER); + l.addWithId(FireworkExplosion.Shape.BURST.getId(), FireworkShapes.BURST, () -> (FireworkShape) (Object) FireworkExplosion.Shape.BURST); }); } From 89310cdb22156c335b90a1823410c1e29c89fc81 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 11 Nov 2024 21:34:34 +0200 Subject: [PATCH 07/24] Fix Chunk#entities returning all entities of the world --- .../common/world/volume/VolumeStreamUtils.java | 4 ++-- .../world/level/chunk/LevelChunkMixin_API.java | 14 +++++++++----- .../entityactivation/EntityActivationRange.java | 3 +-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java b/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java index b6f2f0b20dd..acd61727b3a 100644 --- a/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java +++ b/src/main/java/org/spongepowered/common/world/volume/VolumeStreamUtils.java @@ -219,14 +219,14 @@ public static void validateStreamArgs(final Vector3i min, final Vector3i max, fi ) { if (chunk.getLevel() instanceof ServerLevel) { return ((PersistentEntitySectionManagerAccessor) ((ServerLevelAccessor) chunk.getLevel()).accessor$getEntityManager()).accessor$sectionStorage() - .getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()) + .getExistingSectionsInChunk(chunk.getPos().toLong()) .flatMap(EntitySection::getEntities) .filter(entity -> VecHelper.inBounds(entity.blockPosition(), min, max)) .map(entity -> new AbstractMap.SimpleEntry<>(entity.blockPosition(), entity)); } else if (Sponge.isClientAvailable() && chunk.getLevel() instanceof ClientLevel) { return ((TransientEntitySectionManagerAccessor) ((ClientLevelAccessor) chunk.getLevel()).accessor$getEntityStorage()) .accessor$sectionStorage() - .getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()) + .getExistingSectionsInChunk(chunk.getPos().toLong()) .flatMap(EntitySection::getEntities) .filter(entity -> VecHelper.inBounds(entity.blockPosition(), min, max)) .map(entity -> new AbstractMap.SimpleEntry<>(entity.blockPosition(), entity)); diff --git a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java index 29080211aac..e8bb8cbfce8 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java +++ b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java @@ -39,6 +39,9 @@ import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.blending.BlendingData; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -71,7 +74,9 @@ import org.spongepowered.asm.mixin.Intrinsic; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.common.accessor.server.level.ServerLevelAccessor; import org.spongepowered.common.accessor.world.level.LevelAccessor; +import org.spongepowered.common.accessor.world.level.entity.PersistentEntitySectionManagerAccessor; import org.spongepowered.common.bridge.world.level.LevelBridge; import org.spongepowered.common.bridge.world.level.chunk.LevelChunkBridge; import org.spongepowered.common.data.holder.SpongeServerLocationBaseDataHolder; @@ -100,7 +105,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; @Mixin(net.minecraft.world.level.chunk.LevelChunk.class) @Implements(@Interface(iface = WorldChunk.class, prefix = "worldChunk$", remap = Interface.Remap.NONE)) @@ -418,12 +422,12 @@ public Optional entity(final UUID uuid) { .filter(x -> x.chunkPosition().equals(this.chunkPos)); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Collection entities() { - return (Collection) (Object) StreamSupport.stream( - ((LevelAccessor) this.level).invoker$getEntities().getAll().spliterator(), false) - .collect(Collectors.toList()); + final PersistentEntitySectionManager entityManager = ((ServerLevelAccessor) this.level).accessor$getEntityManager(); + final EntitySectionStorage entitySectionStorage = ((PersistentEntitySectionManagerAccessor) entityManager).accessor$sectionStorage(); + return (Collection) entitySectionStorage.getExistingSectionsInChunk(this.chunkPos.toLong()).flatMap(EntitySection::getEntities).toList(); } @SuppressWarnings({"rawtypes", "unchecked"}) diff --git a/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java b/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java index 0e1337201d6..6f123326cde 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java +++ b/src/mixins/java/org/spongepowered/common/mixin/plugin/entityactivation/EntityActivationRange.java @@ -25,7 +25,6 @@ package org.spongepowered.common.mixin.plugin.entityactivation; import com.google.common.collect.ImmutableMap; -import net.minecraft.core.SectionPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -262,7 +261,7 @@ public static void activateEntities(final ServerLevel world) { private static void activateChunkEntities(final ServerPlayer player, final LevelChunk chunk) { final PersistentEntitySectionManager entityManager = ((ServerLevelAccessor) chunk.getLevel()).accessor$getEntityManager(); final EntitySectionStorage entitySectionStorage = ((PersistentEntitySectionManagerAccessor) entityManager).accessor$sectionStorage(); - entitySectionStorage.getExistingSectionsInChunk(SectionPos.of(chunk.getPos(), 0).asLong()).flatMap(EntitySection::getEntities).forEach(entity -> { + entitySectionStorage.getExistingSectionsInChunk(chunk.getPos().toLong()).flatMap(EntitySection::getEntities).forEach(entity -> { if (!entity.chunkPosition().equals(chunk.getPos())) { return; } From 43f6ac3a8e4e4c0e21f2c1f24edde678f6dbfdb5 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 11 Nov 2024 21:35:37 +0200 Subject: [PATCH 08/24] Fix ChunkEvent#Unload fired too late --- .../core/server/level/ChunkHolderMixin.java | 34 ++++++++++++++++--- .../core/server/level/ChunkMapMixin.java | 14 -------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java index a58cb5b70d4..6b3535cd498 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java @@ -53,18 +53,44 @@ abstract class ChunkHolderMixin extends GenerationChunkHolderMixin { @Shadow public abstract CompletableFuture> shadow$getEntityTickingChunkFuture(); // @formatter:on + private LevelChunk impl$loadedChunk; + /** * After onFullChunkStatusChange */ @Inject(method = "lambda$scheduleFullChunkPromotion$4", at = @At("TAIL")) private void impl$onScheduleFullChunkPromotion(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { - if ($$1x == FullChunkStatus.ENTITY_TICKING && ShouldFire.CHUNK_EVENT_LOAD) { + if ($$1x == FullChunkStatus.ENTITY_TICKING) { this.shadow$getEntityTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(chunk -> { - final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); - final ChunkEvent.Load event = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(), + this.impl$loadedChunk = chunk; + if (ShouldFire.CHUNK_EVENT_LOAD) { + final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); + final ChunkEvent.Load event = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(), (WorldChunk) chunk, chunkPos, (ResourceKey) (Object) chunk.getLevel().dimension().location()); - SpongeCommon.post(event); + SpongeCommon.post(event); + } }); } } + + @Inject(method = "demoteFullChunk", at = @At("HEAD")) + private void impl$onDemoteFullChunkPre(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { + if (this.impl$loadedChunk != null && ShouldFire.CHUNK_EVENT_UNLOAD_PRE) { + final Vector3i chunkPos = VecHelper.toVector3i(this.impl$loadedChunk.getPos()); + final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPre(PhaseTracker.getInstance().currentCause(), + (WorldChunk) this.impl$loadedChunk, chunkPos, (ResourceKey) (Object) this.impl$loadedChunk.getLevel().dimension().location()); + SpongeCommon.post(event); + } + } + + @Inject(method = "demoteFullChunk", at = @At("TAIL")) + private void impl$onDemoteFullChunkPost(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) { + if (this.impl$loadedChunk != null && ShouldFire.CHUNK_EVENT_UNLOAD_POST) { + final Vector3i chunkPos = VecHelper.toVector3i(this.impl$loadedChunk.getPos()); + final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPost(PhaseTracker.getInstance().currentCause(), chunkPos, + (ResourceKey) (Object) this.impl$loadedChunk.getLevel().dimension().location()); + SpongeCommon.post(event); + } + this.impl$loadedChunk = null; + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java index 4780c5edbcd..2a19edb8b3e 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java @@ -117,14 +117,6 @@ public abstract class ChunkMapMixin implements ChunkMapBridge { ) ) private void impl$onSetUnloaded(final ServerLevel level, final LevelChunk chunk) { - final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos()); - - if (ShouldFire.CHUNK_EVENT_UNLOAD_PRE) { - final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPre(PhaseTracker.getInstance().currentCause(), - (WorldChunk) chunk, chunkPos, (ResourceKey) (Object) this.level.dimension().location()); - SpongeCommon.post(event); - } - level.unload(chunk); for (final Direction dir : Constants.Chunk.CARDINAL_DIRECTIONS) { @@ -136,12 +128,6 @@ public abstract class ChunkMapMixin implements ChunkMapBridge { ((LevelChunkBridge) neighbor).bridge$setNeighborChunk(oppositeIndex, null); } } - - if (ShouldFire.CHUNK_EVENT_UNLOAD_POST) { - final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPost(PhaseTracker.getInstance().currentCause(), chunkPos, - (ResourceKey) (Object) this.level.dimension().location()); - SpongeCommon.post(event); - } } @Inject(method = "save", at = @At(value = "HEAD"), cancellable = true) From dbdc282bf8f291ce84c71927493846a186b27a23 Mon Sep 17 00:00:00 2001 From: Nel <57587152+nelind3@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:29:23 +0100 Subject: [PATCH 09/24] Update ServerLevelMixin::save to match the vanilla impl again (#4148) --- .../common/mixin/core/server/level/ServerLevelMixin.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java index 3f7af4b2ea8..ce4e2f2b124 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerLevelMixin.java @@ -46,6 +46,7 @@ import net.minecraft.sounds.SoundEvents; import net.minecraft.util.ProgressListener; import net.minecraft.world.RandomSequences; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.player.Player; @@ -60,6 +61,7 @@ import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.end.EndDragonFight; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.storage.LevelStorageSource; @@ -136,6 +138,7 @@ public abstract class ServerLevelMixin extends LevelMixin implements ServerLevel // @formatter:off @Shadow @Final private ServerLevelData serverLevelData; + @Shadow @Final private PersistentEntitySectionManager entityManager; @Shadow @Final private LevelTicks blockTicks; @Shadow @Final private LevelTicks fluidTicks; @Shadow private int emptyTime; @@ -398,6 +401,12 @@ public void save(@Nullable final ProgressListener progress, final boolean flush, chunkProvider.save(flush); } + if (flush) { + this.entityManager.saveAll(); + } else { + this.entityManager.autoSave(); + } + Sponge.eventManager().post(SpongeEventFactory.createSaveWorldEventPost(currentCause, ((ServerWorld) this))); } } From 8c20e204ba011a7453ba6f7c3c183ce94063de02 Mon Sep 17 00:00:00 2001 From: aromaa Date: Thu, 14 Nov 2024 19:06:47 +0200 Subject: [PATCH 10/24] Fix maps --- .../common/map/decoration/SpongeMapDecorationBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/spongepowered/common/map/decoration/SpongeMapDecorationBuilder.java b/src/main/java/org/spongepowered/common/map/decoration/SpongeMapDecorationBuilder.java index f66e259aeba..1b65b346685 100644 --- a/src/main/java/org/spongepowered/common/map/decoration/SpongeMapDecorationBuilder.java +++ b/src/main/java/org/spongepowered/common/map/decoration/SpongeMapDecorationBuilder.java @@ -125,7 +125,7 @@ public MapDecoration.Builder fromContainer(final DataView container) { final byte rot = this.getByteFromContainer(container, Constants.Map.DECORATION_ROTATION); - final var mcType = BuiltInRegistries.MAP_DECORATION_TYPE.get((ResourceLocation) (Object) type); + final var mcType = BuiltInRegistries.MAP_DECORATION_TYPE.getValue((ResourceLocation) (Object) type); if (mcType == null) { throw new IllegalStateException("Missing a MapDecorationType, could not find one for Minecraft's MapDecoration.Type: " + type); } From 7a43d1aaaae20857113dff5c881f3545d50c5d65 Mon Sep 17 00:00:00 2001 From: aromaa Date: Thu, 14 Nov 2024 19:08:07 +0200 Subject: [PATCH 11/24] Implement DataTranslatable TypeSerializer --- .../DataTranslatableTypeSerializer.java | 73 +++++++++++++++++++ .../common/config/PluginConfigManager.java | 3 +- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/spongepowered/common/config/DataTranslatableTypeSerializer.java diff --git a/src/main/java/org/spongepowered/common/config/DataTranslatableTypeSerializer.java b/src/main/java/org/spongepowered/common/config/DataTranslatableTypeSerializer.java new file mode 100644 index 00000000000..9ff9379395f --- /dev/null +++ b/src/main/java/org/spongepowered/common/config/DataTranslatableTypeSerializer.java @@ -0,0 +1,73 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.config; + +import io.leangen.geantyref.GenericTypeReflector; +import org.spongepowered.api.data.DataManager; +import org.spongepowered.api.data.persistence.DataTranslator; +import org.spongepowered.common.data.persistence.ConfigurateTranslator; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; + +import javax.inject.Inject; + +public final class DataTranslatableTypeSerializer implements TypeSerializer { + + private final DataManager dataManager; + + @Inject + DataTranslatableTypeSerializer(final DataManager dataManager) { + this.dataManager = dataManager; + } + + public boolean accepts(final Type x) { + return this.dataManager.translator(GenericTypeReflector.erase(x)).isPresent(); + } + + @SuppressWarnings("unchecked") + @Override + public Object deserialize(final Type type, final ConfigurationNode value) throws SerializationException { + final Class clazz = (Class) GenericTypeReflector.erase(type); + final DataTranslator translator = this.dataManager.translator(clazz) + .orElseThrow(() -> new SerializationException("Could not translate of DataTranslatable type: " + clazz.getName())); + return translator.translate(ConfigurateTranslator.instance().translate(value)); + } + + @SuppressWarnings("unchecked") + @Override + public void serialize(final Type type, final Object obj, final ConfigurationNode value) throws SerializationException { + if (obj == null) { + value.raw(null); + } else { + final Class clazz = (Class) GenericTypeReflector.erase(type); + final DataTranslator translator = this.dataManager.translator(clazz) + .orElseThrow(() -> new SerializationException("Could not translate of DataTranslatable type: " + clazz.getName())); + ConfigurateTranslator.instance().translateDataToNode(value, translator.translate(obj)); + } + } +} diff --git a/src/main/java/org/spongepowered/common/config/PluginConfigManager.java b/src/main/java/org/spongepowered/common/config/PluginConfigManager.java index 60389388981..3fb5af7a96c 100644 --- a/src/main/java/org/spongepowered/common/config/PluginConfigManager.java +++ b/src/main/java/org/spongepowered/common/config/PluginConfigManager.java @@ -49,7 +49,7 @@ public final class PluginConfigManager implements ConfigManager { private final WatchServiceListener listener; @Inject - PluginConfigManager(final DataSerializableTypeSerializer dataSerializableSerializer) throws IOException { + PluginConfigManager(final DataSerializableTypeSerializer dataSerializableSerializer, final DataTranslatableTypeSerializer dataTranslatableSerializer) throws IOException { // TODO: Move this onto the async scheduler, rather than shared FJ pool? this.listener = WatchServiceListener.builder() .threadFactory(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Sponge-WatchService-%d").build()) @@ -60,6 +60,7 @@ public final class PluginConfigManager implements ConfigManager { // See https://github.com/SpongePowered/SpongeCommon/issues/1348 .register(DataSerializableTypeSerializer::accepts, dataSerializableSerializer) .registerAll(SpongeAdventure.CONFIGURATE.serializers()) + .register(dataTranslatableSerializer::accepts, dataTranslatableSerializer) .build(); } From bc6baadaee9c0507bb33f50ad67fb6b7d7a5e055 Mon Sep 17 00:00:00 2001 From: Nel <57587152+nelind3@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:53:25 +0100 Subject: [PATCH 12/24] Replace RunAroundLikeCrazyGoalMixin::tick overwrite with injects (#4151) --- .../ai/goal/RunAroundLikeCrazyGoalMixin.java | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/ai/goal/RunAroundLikeCrazyGoalMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/ai/goal/RunAroundLikeCrazyGoalMixin.java index 82e86c1939a..f9055bde296 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/ai/goal/RunAroundLikeCrazyGoalMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/ai/goal/RunAroundLikeCrazyGoalMixin.java @@ -24,19 +24,18 @@ */ package org.spongepowered.common.mixin.core.world.entity.ai.goal; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.goal.RunAroundLikeCrazyGoal; import net.minecraft.world.entity.animal.horse.AbstractHorse; -import net.minecraft.world.entity.player.Player; import org.spongepowered.api.entity.living.animal.horse.HorseLike; import org.spongepowered.api.event.CauseStackManager; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.entity.DismountTypes; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.SpongeCommon; import org.spongepowered.common.bridge.world.entity.EntityBridge; import org.spongepowered.common.event.tracking.PhaseTracker; @@ -45,51 +44,40 @@ public abstract class RunAroundLikeCrazyGoalMixin extends GoalMixin { // @formatter:off - @Shadow @Final @Mutable private AbstractHorse horse; + @Shadow @Final private AbstractHorse horse; // @formatter:on - /** - * @author rexbut - December 16th, 2016 - * @author i509VCB - February 18th, 2020 - 1.14.4 - * - * @reason - adjusted to support {@link DismountTypes} - */ - @Overwrite - public void tick() { - if (!this.horse.isTamed() && this.horse.getRandom().nextInt(50) == 0) { - Entity entity = this.horse.getPassengers().get(0); - - if (entity == null) { - return; - } - - if (entity instanceof Player) { - int i = this.horse.getTemper(); - int j = this.horse.getMaxTemper(); - - if (j > 0 && this.horse.getRandom().nextInt(j) < i) { - // Sponge start - Fire Tame Entity event - try (CauseStackManager.StackFrame frame = PhaseTracker.getCauseStackManager().pushCauseFrame()) { - frame.pushCause(entity); - if (SpongeCommon.post(SpongeEventFactory.createTameEntityEvent(frame.currentCause(), (HorseLike) this.horse))) { - return; - } - } - // Sponge end - this.horse.tameWithName((Player)entity); - return; - } - - this.horse.modifyTemper(5); + @Inject( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/animal/horse/AbstractHorse;tameWithName(Lnet/minecraft/world/entity/player/Player;)Z" + ), + cancellable = true + ) + private void impl$throwTameEntityEvent(final CallbackInfo ci) { + try (CauseStackManager.StackFrame frame = PhaseTracker.getCauseStackManager().pushCauseFrame()) { + frame.pushCause(this.horse.getFirstPassenger()); + if (SpongeCommon.post(SpongeEventFactory.createTameEntityEvent(frame.currentCause(), (HorseLike) this.horse))) { + ci.cancel(); } + } + } - // Sponge start - Throw an event before calling entity states - // this.horseHost.ejectPassengers(); // Vanilla - if (((EntityBridge) this.horse).bridge$removePassengers(DismountTypes.DERAIL.get())) { - // Sponge end - this.horse.makeMad(); - this.horse.level().broadcastEntityEvent(this.horse, (byte)6); - } + @Inject( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/animal/horse/AbstractHorse;ejectPassengers()V" + ), + cancellable = true + ) + private void impl$handleDismountTypes(final CallbackInfo ci) { + if (((EntityBridge) this.horse).bridge$removePassengers(DismountTypes.DERAIL.get())) { + this.horse.makeMad(); + this.horse.level().broadcastEntityEvent(this.horse, (byte)6); } + + ci.cancel(); } } From 467fe0e6c40fbe6d23868977e679f379f0ff9cea Mon Sep 17 00:00:00 2001 From: aromaa Date: Fri, 15 Nov 2024 22:30:44 +0200 Subject: [PATCH 13/24] Clean up SpongeUserManager knownUUIDs opts Fixes logic bug where the set was not being filled correctly due to trying to parse full path instead of the file name. Moved all of the logic to its own class. Correctly handle overflows and re-read the filesystem. --- .../common/user/SpongeUserFileCache.java | 238 ++++++++++++++++++ .../common/user/SpongeUserManager.java | 171 ++----------- .../user/SpongeUserMutableWatchEvent.java | 56 ----- 3 files changed, 253 insertions(+), 212 deletions(-) create mode 100644 src/main/java/org/spongepowered/common/user/SpongeUserFileCache.java delete mode 100644 src/main/java/org/spongepowered/common/user/SpongeUserMutableWatchEvent.java diff --git a/src/main/java/org/spongepowered/common/user/SpongeUserFileCache.java b/src/main/java/org/spongepowered/common/user/SpongeUserFileCache.java new file mode 100644 index 00000000000..72e5e475bc0 --- /dev/null +++ b/src/main/java/org/spongepowered/common/user/SpongeUserFileCache.java @@ -0,0 +1,238 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.user; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.common.SpongeCommon; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This is an optimization for frequent calls to + * retrieve all the players who have visited + * the server. This could be part of tab completion + * where good server performance is critical. + * + * While one could achieve the same with other + * relevant caches, the file system is always the + * source of truth. + */ +final class SpongeUserFileCache { + + private final Supplier path; + + private Set knownUniqueIds = new HashSet<>(); + + private @Nullable WatchService watchService = null; + private @Nullable WatchKey watchKey = null; + + SpongeUserFileCache(final Supplier path) { + this.path = path; + } + + public void init() { + final Path path = this.path.get(); + this.shutdownWatcher(); + try { + this.watchService = path.getFileSystem().newWatchService(); + this.watchKey = path.register(this.watchService, + StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + } catch (final IOException e) { + SpongeCommon.logger().warn("Could not start file watcher", e); + this.shutdownWatcher(); + return; + } + + this.scanFiles(path); + } + + private void scanFiles(final Path path) { + if (!Files.isDirectory(path)) { + return; + } + + try (final Stream list = Files.list(path)) { + this.knownUniqueIds = list.map(SpongeUserFileCache::getUniqueIdFromPath) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } catch (final IOException e) { + SpongeCommon.logger().error("Failed to get player files", e); + return; + } + + this.pollFilesystemWatcher(true); + } + + private void pollFilesystemWatcher() { + this.pollFilesystemWatcher(false); + } + + private void pollFilesystemWatcher(final boolean initialPoll) { + if (this.watchKey == null || !this.watchKey.isValid()) { + if (!initialPoll) { + // Reboot this if it's somehow failed. + this.init(); + } + return; + } + + // We've already got the UUIDs, so we need to just see if the file system + // watcher has found anymore (or removed any). + final Map watcherUpdateMap = new HashMap<>(); + for (final WatchEvent event : this.watchKey.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + if (!initialPoll) { + this.scanFiles(this.path.get()); + } else { + this.watchKey.cancel(); + } + return; + } + + @SuppressWarnings("unchecked") final WatchEvent ev = (WatchEvent) event; + final @Nullable Path file = ev.context(); + + // It is possible that the context is null, in which case, ignore it. + if (file != null) { + final String filename = file.getFileName().toString(); + + // We don't determine the UUIDs yet, we'll only do that if we need to. + watcherUpdateMap.computeIfAbsent(filename, f -> new MutableWatchEvent()).set(ev.kind()); + } + } + + // Now we know what the final result is, we can act upon it. + for (final Map.Entry entry : watcherUpdateMap.entrySet()) { + final WatchEvent.Kind kind = entry.getValue().get(); + if (kind == null) { + continue; + } + + final @Nullable UUID uuid = SpongeUserFileCache.getUniqueIdFromPath(entry.getKey()); + if (uuid == null) { + continue; + } + + // It will only be create or delete here. + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + this.knownUniqueIds.add(uuid); + } else { + this.knownUniqueIds.remove(uuid); + } + } + } + + public void userCreated(final UUID uniqueId) { + this.pollFilesystemWatcher(); + this.knownUniqueIds.add(uniqueId); + } + + public boolean contains(final UUID uniqueId) { + this.pollFilesystemWatcher(); + return this.knownUniqueIds.contains(uniqueId); + } + + public Stream knownUUIDs() { + this.pollFilesystemWatcher(); + return this.knownUniqueIds.stream(); + } + + public void shutdownWatcher() { + if (this.watchKey != null) { + this.watchKey.cancel(); + this.watchKey = null; + } + + if (this.watchService != null) { + try { + this.watchService.close(); + } catch (final IOException ignored) { + } + + this.watchService = null; + } + } + + private static @Nullable UUID getUniqueIdFromPath(final Path path) { + return SpongeUserFileCache.getUniqueIdFromPath(path.getFileName().toString()); + } + + private static @Nullable UUID getUniqueIdFromPath(final String fileName) { + final String[] parts = fileName.split("\\.", 2); + if (parts.length != 2 || parts[0].length() != 36 || !parts[1].equals("dat")) { + return null; + } + try { + return UUID.fromString(parts[0]); + } catch (final IllegalArgumentException ignored) { + return null; + } + } + + /** + * Filters that sequences of CREATE -> DELETE + * or DELETE -> CREATE do not raise changes. + */ + private static final class MutableWatchEvent { + + private WatchEvent.Kind kind = null; + + public WatchEvent.@Nullable Kind get() { + return this.kind; + } + + public void set(WatchEvent.Kind kind) { + if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { + // This should never happen, we don't listen to this. + // However, if it does, treat it as a create, because it + // infers the existence of the file. + kind = StandardWatchEventKinds.ENTRY_CREATE; + } + + if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_DELETE) { + if (this.kind != null && this.kind != kind) { + this.kind = null; + } else { + this.kind = kind; + } + } + } + } +} diff --git a/src/main/java/org/spongepowered/common/user/SpongeUserManager.java b/src/main/java/org/spongepowered/common/user/SpongeUserManager.java index 7dd9080a2a4..c550c5082a1 100644 --- a/src/main/java/org/spongepowered/common/user/SpongeUserManager.java +++ b/src/main/java/org/spongepowered/common/user/SpongeUserManager.java @@ -28,7 +28,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.PlayerDataStorage; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -40,7 +39,6 @@ import org.spongepowered.api.profile.GameProfileCache; import org.spongepowered.api.user.UserManager; import org.spongepowered.common.SpongeCommon; -import org.spongepowered.common.accessor.server.MinecraftServerAccessor; import org.spongepowered.common.accessor.server.players.PlayerListAccessor; import org.spongepowered.common.accessor.world.level.storage.PlayerDataStorageAccessor; import org.spongepowered.common.entity.player.SpongeUserData; @@ -48,17 +46,10 @@ import org.spongepowered.common.profile.SpongeGameProfile; import java.io.IOException; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; -import java.util.HashMap; import java.util.HashSet; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -76,14 +67,11 @@ public final class SpongeUserManager implements UserManager { public static final UUID FAKEPLAYER_UUID = UUID.fromString("41C82C87-7AFB-4024-BA57-13D2C99CAE77"); - // This is the important set - this tells us if a User file actually exists, - // it should mirror the filesystem. - private final Set knownUUIDs = new HashSet<>(); + private final SpongeUserFileCache userFileCache; private final Cache userCache = Caffeine.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) .build(); private final Set dirtyUsers = ConcurrentHashMap.newKeySet(); - private final Map watcherUpdateMap = new HashMap<>(); private final MinecraftServer server; private final ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() @@ -91,16 +79,13 @@ public final class SpongeUserManager implements UserManager { .setNameFormat("Sponge-User-Data-Loader") .build()); - private @Nullable WatchService filesystemWatchService = null; - private @Nullable WatchKey watchKey = null; - public SpongeUserManager(final MinecraftServer server) { this.server = server; + this.userFileCache = new SpongeUserFileCache(this::getSaveHandlerDirectory); } public void init() { - this.refreshFilesystemProfiles(); - this.setupWatchers(); + this.userFileCache.init(); } @Override @@ -128,18 +113,18 @@ public CompletableFuture loadOrCreate(final UUID uuid) { if (currentUser != null) { return CompletableFuture.completedFuture(SpongeUserView.create(uuidToUse)); } + if (!always && !this.userFileCache.contains(uuidToUse)) { + return CompletableFuture.completedFuture(null); + } return CompletableFuture.supplyAsync(() -> { - if (always || this.knownUUIDs.contains(uuidToUse)) { - final com.mojang.authlib.@Nullable GameProfile profile = this.server.getProfileCache().get(uuidToUse) - .orElseGet(() -> new com.mojang.authlib.GameProfile(uuidToUse, null)); - try { - this.createUser(profile); - } catch (final IOException e) { - throw new CompletionException(e); - } - return SpongeUserView.create(uuidToUse); + final com.mojang.authlib.@Nullable GameProfile profile = this.server.getProfileCache().get(uuidToUse) + .orElseGet(() -> new com.mojang.authlib.GameProfile(uuidToUse, null)); + try { + this.createUser(profile); + } catch (final IOException e) { + throw new CompletionException(e); } - return null; + return SpongeUserView.create(uuidToUse); }, this.executorService); } @@ -173,7 +158,7 @@ public CompletableFuture> load(final GameProfile profile) { @Override public Stream streamAll() { final GameProfileCache cache = ((Server) this.server).gameProfileManager().cache(); - return this.knownUUIDs.stream().map(x -> cache.findById(x).orElseGet(() -> GameProfile.of(x))); + return this.userFileCache.knownUUIDs().map(x -> cache.findById(x).orElseGet(() -> GameProfile.of(x))); } @Override @@ -271,10 +256,9 @@ public void handlePlayerLogin(final com.mojang.authlib.GameProfile mcProfile) th } private void createUser(final com.mojang.authlib.GameProfile profile) throws IOException { - this.pollFilesystemWatcher(); final @Nullable SpongeUserData user = SpongeUserData.create(profile); this.userCache.put(profile.getId(), user); - this.knownUUIDs.add(profile.getId()); + this.userFileCache.userCreated(profile.getId()); } public void markDirty(final SpongeUserData user) { @@ -286,131 +270,6 @@ public void markDirty(final SpongeUserData user) { } } - // -- Directory Watching - - void setupWatchers() { - this.teardownWatchers(); - // Setup the watch service - try { - this.filesystemWatchService = FileSystems.getDefault().newWatchService(); - this.watchKey = this.getSaveHandlerDirectory().register( - this.filesystemWatchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE); - } catch (final IOException e) { - SpongeCommon.logger().warn("Could not start file watcher"); - if (this.filesystemWatchService != null) { - // it might be the watchKey that failed, so null it out again. - try { - this.filesystemWatchService.close(); - } catch (final IOException ex) { - // ignored - } - } - this.watchKey = null; - this.filesystemWatchService = null; - } - } - - void teardownWatchers() { - if (this.watchKey != null) { - this.watchKey.cancel(); - this.watchKey = null; - } - - if (this.filesystemWatchService != null) { - try { - this.filesystemWatchService.close(); - } catch (final IOException e) { - // ignored - we're nulling this anyway - } finally { - this.filesystemWatchService = null; - } - } - } - - void refreshFilesystemProfiles() { - if (this.watchKey != null && this.watchKey.isValid()) { - this.watchKey.reset(); - } - this.knownUUIDs.clear(); - this.userCache.invalidateAll(); - - // Add all known profiles from the data files - final Path playerDataDir = ((MinecraftServerAccessor) this.server).accessor$storageSource().getLevelPath(LevelResource.PLAYER_DATA_DIR); - if (Files.isDirectory(playerDataDir)) { - try (Stream list = Files.list(playerDataDir)) { - list.map(Path::toString) - .filter(file -> file.endsWith(".dat")) // only .dat files - .map(file -> file.substring(0, file.length() - 4)) - .filter(uuid -> !uuid.contains(".")) // fail fast for invalid uuid - .map(playerUuid -> { - try { - return UUID.fromString(playerUuid); - } catch (final Exception ex) { - return null; - } - }) - .filter(Objects::nonNull) - .forEach(this.knownUUIDs::add); - } catch (IOException e) { - SpongeCommon.logger().error("Failed to get player files"); - } - } - } - - private void pollFilesystemWatcher() { - if (this.watchKey == null || !this.watchKey.isValid()) { - // Reboot this if it's somehow failed. - this.refreshFilesystemProfiles(); - this.setupWatchers(); - return; - } - // We've already got the UUIDs, so we need to just see if the file system - // watcher has found any more (or removed any). - synchronized (this.watcherUpdateMap) { - this.watcherUpdateMap.clear(); - for (final WatchEvent event : this.watchKey.pollEvents()) { - @SuppressWarnings("unchecked") final WatchEvent ev = (WatchEvent) event; - final @Nullable Path file = ev.context(); - - // It is possible that the context is null, in which case, ignore it. - if (file != null) { - final String filename = file.getFileName().toString(); - - // We don't determine the UUIDs yet, we'll only do that if we need to. - this.watcherUpdateMap.computeIfAbsent(filename, f -> new SpongeUserMutableWatchEvent()).set(ev.kind()); - } - } - - // Now we know what the final result is, we can act upon it. - for (final Map.Entry entry : this.watcherUpdateMap.entrySet()) { - final WatchEvent.Kind kind = entry.getValue().get(); - if (kind != null) { - final String name = entry.getKey(); - final UUID uuid; - if (name.endsWith(".dat")) { - try { - uuid = UUID.fromString(name.substring(0, name.length() - 4)); - - // It will only be create or delete here. - if (kind == StandardWatchEventKinds.ENTRY_CREATE) { - this.knownUUIDs.add(uuid); - } else { - this.knownUUIDs.remove(uuid); - // We don't do this, in case we were caught at a bad time. - // Everything else should handle it for us, however. - // this.userCache.invalidate(uuid); - } - } catch (final IllegalArgumentException ex) { - // ignored, file isn't of use to us. - } - } - } - } - } - } - private @Nullable Path getPlayerDataFile(final UUID uniqueId) { // Note: Uses the overworld's player data final Path file = this.getSaveHandlerDirectory().resolve(uniqueId + ".dat"); diff --git a/src/main/java/org/spongepowered/common/user/SpongeUserMutableWatchEvent.java b/src/main/java/org/spongepowered/common/user/SpongeUserMutableWatchEvent.java deleted file mode 100644 index 4f7accdfdfb..00000000000 --- a/src/main/java/org/spongepowered/common/user/SpongeUserMutableWatchEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.common.user; - -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; - -// Used to reduce the number of calls to maps. -final class SpongeUserMutableWatchEvent { - - private WatchEvent.Kind kind = null; - - public WatchEvent.Kind get() { - return this.kind; - } - - public void set(WatchEvent.Kind kind) { - if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { - // This should never happen, we don't listen to this. - // However, if it does, treat it as a create, because it - // infers the existence of the file. - kind = StandardWatchEventKinds.ENTRY_CREATE; - } - - if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_DELETE) { - if (this.kind != null && this.kind != kind) { - this.kind = null; - } else { - this.kind = kind; - } - } - } - -} From 49821cc78a3d78d6c30e76c0d606fa6d7f4c6096 Mon Sep 17 00:00:00 2001 From: aromaa Date: Sat, 16 Nov 2024 17:21:41 +0200 Subject: [PATCH 14/24] Fix offline player inventory saving --- .../common/entity/player/SpongeUserInventory.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/spongepowered/common/entity/player/SpongeUserInventory.java b/src/main/java/org/spongepowered/common/entity/player/SpongeUserInventory.java index 9a08c90c14b..66d5d6c6595 100644 --- a/src/main/java/org/spongepowered/common/entity/player/SpongeUserInventory.java +++ b/src/main/java/org/spongepowered/common/entity/player/SpongeUserInventory.java @@ -139,8 +139,7 @@ public ListTag writeList(final ListTag nbtTagListIn) { if (!this.mainInventory.get(i).isEmpty()) { final CompoundTag nbttagcompound = new CompoundTag(); nbttagcompound.putByte("Slot", (byte) i); - this.mainInventory.get(i).save(SpongeCommon.server().registryAccess(), nbttagcompound); - nbtTagListIn.add(nbttagcompound); + nbtTagListIn.add(this.mainInventory.get(i).save(SpongeCommon.server().registryAccess(), nbttagcompound)); } } @@ -148,8 +147,7 @@ public ListTag writeList(final ListTag nbtTagListIn) { if (!this.armorInventory.get(j).isEmpty()) { final CompoundTag nbttagcompound1 = new CompoundTag(); nbttagcompound1.putByte("Slot", (byte) (j + 100)); - this.armorInventory.get(j).save(SpongeCommon.server().registryAccess(), nbttagcompound1); - nbtTagListIn.add(nbttagcompound1); + nbtTagListIn.add(this.armorInventory.get(j).save(SpongeCommon.server().registryAccess(), nbttagcompound1)); } } @@ -157,8 +155,7 @@ public ListTag writeList(final ListTag nbtTagListIn) { if (!this.offHandInventory.get(k).isEmpty()) { final CompoundTag nbttagcompound2 = new CompoundTag(); nbttagcompound2.putByte("Slot", (byte) (k + 150)); - this.offHandInventory.get(k).save(SpongeCommon.server().registryAccess(), nbttagcompound2); - nbtTagListIn.add(nbttagcompound2); + nbtTagListIn.add(this.offHandInventory.get(k).save(SpongeCommon.server().registryAccess(), nbttagcompound2)); } } From e4484ca486582e3ccc4835be87e75190c33ad08d Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 18 Nov 2024 18:29:54 +0200 Subject: [PATCH 15/24] Fix trying to save unserializable root vehicle --- .../common/mixin/core/server/level/ServerPlayerMixin.java | 6 ++++++ .../common/mixin/core/server/players/PlayerListMixin.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerPlayerMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerPlayerMixin.java index 4efd2a4cfe2..86fa33ca360 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerPlayerMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ServerPlayerMixin.java @@ -128,6 +128,7 @@ import org.spongepowered.common.accessor.world.level.portal.DimensionTransitionAccessor; import org.spongepowered.common.adventure.SpongeAdventure; import org.spongepowered.common.bridge.data.DataCompoundHolder; +import org.spongepowered.common.bridge.data.TransientBridge; import org.spongepowered.common.bridge.permissions.SubjectBridge; import org.spongepowered.common.bridge.server.ServerScoreboardBridge; import org.spongepowered.common.bridge.server.level.ServerPlayerBridge; @@ -938,4 +939,9 @@ public Entity changeDimension(final DimensionTransition transition) { ((DimensionTransitionAccessor) (Object) cir.getReturnValue()).accessor$newLevel(this.impl$respawnLevel); this.impl$respawnLevel = null; } + + @Redirect(method = "addAdditionalSaveData", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;hasExactlyOnePlayerPassenger()Z")) + private boolean impl$skipUnserializableRootVehicle(final Entity instance) { + return instance.hasExactlyOnePlayerPassenger() && !((TransientBridge) instance).bridge$isTransient(); + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/players/PlayerListMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/players/PlayerListMixin.java index 76d5070d026..c57f620843e 100755 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/players/PlayerListMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/players/PlayerListMixin.java @@ -680,4 +680,12 @@ public abstract class PlayerListMixin implements PlayerListBridge { ci.cancel(); } } + + @Redirect(method = "remove", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;hasExactlyOnePlayerPassenger()Z")) + private boolean impl$skipUnserializableRootVehicle(final Entity instance) { + //Intentionally only checking for the entity type and + //not for the TRANSIENT key. This ensures that arbitrary + //entities are removed with the player but players are ignored. + return instance.hasExactlyOnePlayerPassenger() && instance.getType().canSerialize(); + } } From 7d3050f98615fde7c1822d9926883b5f471de127 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 18 Nov 2024 18:31:33 +0200 Subject: [PATCH 16/24] Adjust player rotation cancellation code --- .../ServerboundMovePlayerPacketAccessor.java | 7 ++++- .../ServerGamePacketListenerImplMixin.java | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/accessors/java/org/spongepowered/common/accessor/network/protocol/game/ServerboundMovePlayerPacketAccessor.java b/src/accessors/java/org/spongepowered/common/accessor/network/protocol/game/ServerboundMovePlayerPacketAccessor.java index 4cca0fa081d..40a4da5f17b 100644 --- a/src/accessors/java/org/spongepowered/common/accessor/network/protocol/game/ServerboundMovePlayerPacketAccessor.java +++ b/src/accessors/java/org/spongepowered/common/accessor/network/protocol/game/ServerboundMovePlayerPacketAccessor.java @@ -26,6 +26,7 @@ import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(ServerboundMovePlayerPacket.class) @@ -35,7 +36,11 @@ public interface ServerboundMovePlayerPacketAccessor { @Accessor("y") double accessor$y(); - @Accessor("z") double accessor$z(); + @Accessor("yRot") @Mutable void accessor$yRot(float yRot); + + @Accessor("xRot") @Mutable void accessor$xRot(float xRot); + + @Accessor("hasRot") @Mutable void accessor$hasRot(boolean hasRot); } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java index 9b17229b8f5..1186b1058bd 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java @@ -88,8 +88,10 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.Slice; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.SpongeCommon; +import org.spongepowered.common.accessor.network.protocol.game.ServerboundMovePlayerPacketAccessor; import org.spongepowered.common.accessor.network.protocol.game.ServerboundMoveVehiclePacketAccessor; import org.spongepowered.common.accessor.server.level.ServerPlayerGameModeAccessor; import org.spongepowered.common.adventure.SpongeAdventure; @@ -197,9 +199,11 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack } @Inject(method = "handleMovePlayer", - at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;isPassenger()Z"), - cancellable = true - ) + at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/game/ServerboundMovePlayerPacket;getYRot(F)F"), + cancellable = true, + slice = @Slice( + from = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerGamePacketListenerImpl;updateAwaitingTeleport()Z"), + to = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;isPassenger()Z"))) private void impl$callMoveEntityEvent(final ServerboundMovePlayerPacket packetIn, final CallbackInfo ci) { final boolean fireMoveEvent = packetIn.hasPosition(); final boolean fireRotationEvent = packetIn.hasRotation(); @@ -266,7 +270,7 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack } // Handle event results - if (!toPosition.equals(originalToPosition) || !toRotation.equals(originalToRotation)) { + if (!toPosition.equals(originalToPosition)) { // Notify the client about the new position and new rotation. // Both are relatives so the client will keep its momentum. // The client thinks its current position is originalToPosition so the new position is relative to that. @@ -278,6 +282,20 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack (float) toRotation.y(), (float) toRotation.x(), EnumSet.allOf(RelativeMovement.class)); ci.cancel(); + } else if (!toRotation.equals(originalToRotation)) { + // Notify the client about the new rotation. + // Both are relatives so the client will keep its momentum. + // The rotation values can be out of "valid" range so set them directly to the same value the client has. + this.player.setXRot((float) originalToRotation.x()); + this.player.setYRot((float) originalToRotation.y()); + this.shadow$teleport(fromPosition.x(), fromPosition.y(), fromPosition.z(), + (float) toRotation.y(), (float) toRotation.x(), + EnumSet.allOf(RelativeMovement.class)); + + // Let MC handle the movement but override the rotation. + ((ServerboundMovePlayerPacketAccessor) packetIn).accessor$yRot((float) toRotation.y()); + ((ServerboundMovePlayerPacketAccessor) packetIn).accessor$xRot((float) toRotation.x()); + ((ServerboundMovePlayerPacketAccessor) packetIn).accessor$hasRot(true); } } From 0ee303e3550b549113be27f546703f759bcd72e4 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 18 Nov 2024 20:21:49 +0200 Subject: [PATCH 17/24] Re-enable riding player entities --- .../common/mixin/core/world/entity/EntityMixin.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/EntityMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/EntityMixin.java index fff2eb84924..ffdf5b5fb9b 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/EntityMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/entity/EntityMixin.java @@ -992,4 +992,10 @@ public void stopRiding() { }*/ + @Redirect(method = "startRiding(Lnet/minecraft/world/entity/Entity;Z)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/EntityType;canSerialize()Z")) + private boolean impl$allowRidingAnything(final EntityType instance) { + //Vanilla has started to prevent riding non-serializable entities. + //This results in players being unable to ride other players. + return true; + } } From 3e80501780ae54e2e94e40f88da1585cf50a7a36 Mon Sep 17 00:00:00 2001 From: aromaa Date: Mon, 18 Nov 2024 20:22:09 +0200 Subject: [PATCH 18/24] Fix movement events --- .../ServerGamePacketListenerImplMixin.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java index 6af24aacca1..4dc4445da99 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/network/ServerGamePacketListenerImplMixin.java @@ -265,9 +265,9 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack this.shadow$teleport(new PositionMoveRotation( VecHelper.toVanillaVector3d(fromPosition), Vec3.ZERO, - (float) toRotation.y(), (float) toRotation.x() + (float) (toRotation.y() - originalToRotation.y()), (float) (toRotation.x() - originalToRotation.x()) ), - EnumSet.of(Relative.X_ROT, Relative.Y_ROT) + Relative.ROTATION ); ci.cancel(); return; @@ -283,10 +283,11 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack this.player.setXRot((float) originalToRotation.x()); this.player.setYRot((float) originalToRotation.y()); this.shadow$teleport(new PositionMoveRotation( - VecHelper.toVanillaVector3d(toPosition), + VecHelper.toVanillaVector3d(toPosition.sub(originalToPosition)), Vec3.ZERO, - (float) toRotation.y(), (float) toRotation.x()), - EnumSet.allOf(Relative.class)); + (float) (toRotation.y() - originalToRotation.y()), (float) (toRotation.x() - originalToRotation.x()) + ), + Relative.ALL); ci.cancel(); } else if (!toRotation.equals(originalToRotation)) { // Notify the client about the new rotation. @@ -294,9 +295,12 @@ public abstract class ServerGamePacketListenerImplMixin extends ServerCommonPack // The rotation values can be out of "valid" range so set them directly to the same value the client has. this.player.setXRot((float) originalToRotation.x()); this.player.setYRot((float) originalToRotation.y()); - this.shadow$teleport(fromPosition.x(), fromPosition.y(), fromPosition.z(), - (float) toRotation.y(), (float) toRotation.x(), - EnumSet.allOf(RelativeMovement.class)); + this.shadow$teleport(new PositionMoveRotation( + Vec3.ZERO, + Vec3.ZERO, + (float) (toRotation.y() - originalToRotation.y()), (float) (toRotation.x() - originalToRotation.x()) + ), + EnumSet.of(Relative.X, Relative.Y, Relative.Z, Relative.X_ROT, Relative.Y_ROT, Relative.DELTA_X, Relative.DELTA_Y, Relative.DELTA_Z)); // Let MC handle the movement but override the rotation. ((ServerboundMovePlayerPacketAccessor) packetIn).accessor$yRot((float) toRotation.y()); From 584cdeb97db26efff6461380f27c2a3d2db14a1e Mon Sep 17 00:00:00 2001 From: aromaa Date: Thu, 21 Nov 2024 20:05:58 +0200 Subject: [PATCH 19/24] Wrap mod command argument types in proxy environment --- ...andsPacket_ArgumentNodeStub_IpForward.java | 88 +++++++++++++++++++ .../resources/mixins.sponge.ipforward.json | 1 + 2 files changed, 89 insertions(+) create mode 100644 src/mixins/java/org/spongepowered/common/mixin/ipforward/network/protocol/game/ClientboundCommandsPacket_ArgumentNodeStub_IpForward.java diff --git a/src/mixins/java/org/spongepowered/common/mixin/ipforward/network/protocol/game/ClientboundCommandsPacket_ArgumentNodeStub_IpForward.java b/src/mixins/java/org/spongepowered/common/mixin/ipforward/network/protocol/game/ClientboundCommandsPacket_ArgumentNodeStub_IpForward.java new file mode 100644 index 00000000000..bacf15322f2 --- /dev/null +++ b/src/mixins/java/org/spongepowered/common/mixin/ipforward/network/protocol/game/ClientboundCommandsPacket_ArgumentNodeStub_IpForward.java @@ -0,0 +1,88 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.mixin.ipforward.network.protocol.game; + +import com.mojang.brigadier.arguments.ArgumentType; +import io.netty.buffer.Unpooled; +import net.minecraft.commands.synchronization.ArgumentTypeInfo; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.Nullable; +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; + +/** + * If a proxy server tries to inspect the commands sent + * to the client, it is unable to parse the elements + * that contain mod specific information due to being + * unaware of their structure. + * + * To fix this, the proxy server expects that non-vanilla + * elements are wrapped in a magic argument that it can + * detect to skip its content. + * + * The proxy server is expected to unwrap these arguments + * afterward to allow the client to parse the commands + * as normal. + */ +@Mixin(targets = "net/minecraft/network/protocol/game/ClientboundCommandsPacket$ArgumentNodeStub") +public final class ClientboundCommandsPacket_ArgumentNodeStub_IpForward { + + //Magic value that was chosen by the proxy implementation. + private static final int ipForward$MOD_ARGUMENT_ID = -256; + + @Inject(method = "serializeCap(Lnet/minecraft/network/FriendlyByteBuf;Lnet/minecraft/commands/synchronization/ArgumentTypeInfo;Lnet/minecraft/commands/synchronization/ArgumentTypeInfo$Template;)V", at = @At("HEAD"), cancellable = true) + private static , T extends ArgumentTypeInfo.Template> void ipForward$onSerializeCap( + final FriendlyByteBuf buf, final ArgumentTypeInfo $$1, final ArgumentTypeInfo.Template $$2, final CallbackInfo ci) { + final @Nullable ResourceLocation key = BuiltInRegistries.COMMAND_ARGUMENT_TYPE.getKey($$1); + if (key == null || ClientboundCommandsPacket_ArgumentNodeStub_IpForward.ipForward$isVanillaArgument(key)) { + return; + } + + ci.cancel(); + + final FriendlyByteBuf commandData = new FriendlyByteBuf(Unpooled.buffer()); + $$1.serializeToNetwork((T) $$2, commandData); + + buf.writeVarInt(ClientboundCommandsPacket_ArgumentNodeStub_IpForward.ipForward$MOD_ARGUMENT_ID); + buf.writeVarInt(BuiltInRegistries.COMMAND_ARGUMENT_TYPE.getId($$1)); + buf.writeVarInt(commandData.readableBytes()); + buf.writeBytes(commandData); + } + + private static boolean ipForward$isVanillaArgument(final ResourceLocation key) { + return switch (key.getNamespace()) { + case "brigadier" -> true; + case "minecraft" -> switch (key.getPath()) { + case "test_argument", "test_class" -> false; //These are normally under IS_RUNNING_IN_IDE, proxies cant handle them. + default -> true; + }; + default -> false; + }; + } +} diff --git a/src/mixins/resources/mixins.sponge.ipforward.json b/src/mixins/resources/mixins.sponge.ipforward.json index 954ce31567e..28e291c6a55 100644 --- a/src/mixins/resources/mixins.sponge.ipforward.json +++ b/src/mixins/resources/mixins.sponge.ipforward.json @@ -4,6 +4,7 @@ "plugin": "org.spongepowered.common.mixin.plugin.IpForwardPlugin", "mixins": [ "network.ConnectionMixin_IpForward", + "network.protocol.game.ClientboundCommandsPacket_ArgumentNodeStub_IpForward", "network.protocol.handshake.ClientIntentionPacketMixin_IpForward", "server.MinecraftServerMixin_IpForward", "server.network.ServerHandshakePacketListenerImplMixin_IpForward", From e7019341544ea4734b7f450de10ea6888dc03d0d Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Tue, 19 Nov 2024 17:50:05 +0100 Subject: [PATCH 20/24] Move LibraryManager into a subproject --- build.gradle.kts | 1 + forge/build.gradle.kts | 5 +- .../SpongeForgeDependencyLocator.java | 8 +- .../moddiscovery/library/InstallerUtils.java | 169 ------------ .../moddiscovery/library/LibraryManager.java | 257 ------------------ .../{AsyncUtils.java => Log4JLogger.java} | 38 +-- .../library/model/sponge/Libraries.java | 57 ---- .../model/sponge/SonatypeResponse.java | 46 ---- library-manager/build.gradle.kts | 14 + .../org/spongepowered/libs}/AsyncUtils.java | 8 +- .../spongepowered/libs}/InstallerUtils.java | 22 +- .../spongepowered/libs}/LibraryManager.java | 65 ++--- .../java/org/spongepowered/libs/Logger.java | 69 +++++ .../spongepowered/libs/model}/Libraries.java | 2 +- .../libs/model}/SonatypeResponse.java | 2 +- neoforge/build.gradle.kts | 5 +- .../SpongeNeoDependencyLocator.java | 8 +- .../moddiscovery/library/InstallerUtils.java | 169 ------------ .../moddiscovery/library/LibraryManager.java | 257 ------------------ .../{AsyncUtils.java => Log4JLogger.java} | 38 +-- .../library/model/sponge/Libraries.java | 57 ---- .../model/sponge/SonatypeResponse.java | 46 ---- settings.gradle.kts | 1 + vanilla/build.gradle.kts | 5 +- .../vanilla/installer/Installer.java | 3 + .../vanilla/installer/InstallerMain.java | 21 +- .../vanilla/installer/library/TinyLogger.java | 60 ++++ 27 files changed, 268 insertions(+), 1165 deletions(-) delete mode 100644 forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/InstallerUtils.java delete mode 100644 forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/LibraryManager.java rename forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/{AsyncUtils.java => Log4JLogger.java} (57%) delete mode 100644 forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java delete mode 100644 forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java create mode 100644 library-manager/build.gradle.kts rename {vanilla/src/installer/java/org/spongepowered/vanilla/installer => library-manager/src/main/java/org/spongepowered/libs}/AsyncUtils.java (86%) rename {vanilla/src/installer/java/org/spongepowered/vanilla/installer => library-manager/src/main/java/org/spongepowered/libs}/InstallerUtils.java (89%) rename {vanilla/src/installer/java/org/spongepowered/vanilla/installer => library-manager/src/main/java/org/spongepowered/libs}/LibraryManager.java (79%) create mode 100644 library-manager/src/main/java/org/spongepowered/libs/Logger.java rename {vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge => library-manager/src/main/java/org/spongepowered/libs/model}/Libraries.java (97%) rename {vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge => library-manager/src/main/java/org/spongepowered/libs/model}/SonatypeResponse.java (96%) delete mode 100644 neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/InstallerUtils.java delete mode 100644 neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/LibraryManager.java rename neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/{AsyncUtils.java => Log4JLogger.java} (57%) delete mode 100644 neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java delete mode 100644 neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java create mode 100644 vanilla/src/installer/java/org/spongepowered/vanilla/installer/library/TinyLogger.java diff --git a/build.gradle.kts b/build.gradle.kts index 41b3e33b508..1b8dae2ce14 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -189,6 +189,7 @@ idea { (project as ExtensionAware).extensions["settings"].run { (this as ExtensionAware).extensions.getByType(org.jetbrains.gradle.ext.TaskTriggersConfig::class).run { afterSync(":modlauncher-transformers:build") + afterSync(":library-manager:build") } } } diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 8833114dd0b..ead70d976fa 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -18,7 +18,8 @@ plugins { } val commonProject = parent!! -val transformersProject = parent!!.project(":modlauncher-transformers") +val transformersProject = commonProject.project(":modlauncher-transformers") +val libraryManagerProject = commonProject.project(":library-manager") val testPluginsProject: Project? = rootProject.subprojects.find { "testplugins" == it.name } val apiVersion: String by project @@ -211,6 +212,7 @@ dependencies { service(project(transformersProject.path)) { exclude(group = "cpw.mods", module = "modlauncher") } + service(project(libraryManagerProject.path)) service(platform(apiLibs.configurate.bom)) service(apiLibs.configurate.core) { exclude(group = "org.checkerframework", module = "checker-qual") @@ -232,6 +234,7 @@ dependencies { val serviceShadedLibraries = serviceShadedLibrariesConfig.name serviceShadedLibraries(project(transformersProject.path)) { isTransitive = false } + serviceShadedLibraries(project(libraryManagerProject.path)) { isTransitive = false } val gameShadedLibraries = gameShadedLibrariesConfig.name gameShadedLibraries("org.spongepowered:spongeapi:$apiVersion") { isTransitive = false } diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/SpongeForgeDependencyLocator.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/SpongeForgeDependencyLocator.java index 28a89f3a523..b73e593f3c6 100644 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/SpongeForgeDependencyLocator.java +++ b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/SpongeForgeDependencyLocator.java @@ -33,8 +33,9 @@ import net.minecraftforge.forgespi.locating.IModLocator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.spongepowered.forge.applaunch.loading.moddiscovery.library.LibraryManager; +import org.spongepowered.forge.applaunch.loading.moddiscovery.library.Log4JLogger; import org.spongepowered.forge.applaunch.transformation.SpongeForgeTransformationService; +import org.spongepowered.libs.LibraryManager; import java.nio.file.Path; import java.util.ArrayList; @@ -60,8 +61,8 @@ public List scanMods(Iterable loadedMods) { } this.libraryManager.finishedProcessing(); - for (final LibraryManager.Library library : this.libraryManager.getAll().values()) { - final Path path = library.getFile(); + for (final LibraryManager.Library library : this.libraryManager.getAll("main")) { + final Path path = library.file(); SpongeForgeDependencyLocator.LOGGER.debug("Proposing jar {} as a game library", path); final IModLocator.ModFileOrException fileOrException = createMod(path); @@ -89,6 +90,7 @@ public String name() { public void initArguments(final Map arguments) { final Environment env = Launcher.INSTANCE.environment(); this.libraryManager = new LibraryManager( + new Log4JLogger(LogManager.getLogger(LibraryManager.class)), env.getProperty(SpongeForgeTransformationService.Keys.CHECK_LIBRARY_HASHES.get()).orElse(true), env.getProperty(SpongeForgeTransformationService.Keys.LIBRARIES_DIRECTORY.get()) .orElseThrow(() -> new IllegalStateException("no libraries available")), diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/InstallerUtils.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/InstallerUtils.java deleted file mode 100644 index 2c36eeff408..00000000000 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/InstallerUtils.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.forge.applaunch.loading.moddiscovery.library; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -final class InstallerUtils { - - private static final Logger LOGGER = LogManager.getLogger(); - // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java - private static final char[] hexArray = "0123456789abcdef".toCharArray(); - - private InstallerUtils() { - } - - public static String toHexString(final byte[] bytes) { - final char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - final int v = bytes[j] & 0xFF; - hexChars[j * 2] = InstallerUtils.hexArray[v >>> 4]; - hexChars[j * 2 + 1] = InstallerUtils.hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - public static boolean validateSha1(final String expectedHash, final Path path) throws IOException { - try (final InputStream is = Files.newInputStream(path)) { - return InstallerUtils.validateSha1(expectedHash, is); - } - } - - public static boolean validateSha1(final String expectedHash, final InputStream stream) throws IOException { - final MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-1"); - } catch (final NoSuchAlgorithmException ex) { - throw new AssertionError(ex); // Guaranteed present by MessageDigest spec - } - - final byte[] buf = new byte[4096]; - int read; - - while ((read = stream.read(buf)) != -1) { - digest.update(buf,0, read); - } - - return expectedHash.equals(InstallerUtils.toHexString(digest.digest())); - } - - /** - * Downloads a file. - * - * @param url The file URL - * @param path The local path - * @throws IOException If there is a problem while downloading the file - */ - public static void download(final URL url, final Path path, final boolean requiresRequest) throws IOException { - Files.createDirectories(path.getParent()); - - final String name = path.getFileName().toString(); - - InstallerUtils.LOGGER.info("Downloading {}. This may take a while...", name); - InstallerUtils.LOGGER.info("URL -> <{}>", url); - - if (!requiresRequest) { - try (final ReadableByteChannel in = Channels.newChannel(url.openStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } else { - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - try (final ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } - } - - /** - * Downloads a file and verify its digest. - * - * @param url The file URL - * @param path The local path - * @param expected The SHA-1 expected digest - * @throws IOException If there is a problem while downloading the file - */ - public static void downloadCheckHash(final URL url, final Path path, final MessageDigest digest, final String expected, - final boolean requiresRequest) throws IOException { - Files.createDirectories(path.getParent()); - - final String name = path.getFileName().toString(); - - InstallerUtils.LOGGER.info("Downloading {}. This may take a while...", name); - InstallerUtils.LOGGER.debug("URL -> <{}>", url); - - if (!requiresRequest) { - // Pipe the download stream into the file and compute the hash - try (final DigestInputStream stream = new DigestInputStream(url.openStream(), digest); final ReadableByteChannel in = Channels - .newChannel(stream); final FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } else { - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - // Pipe the download stream into the file and compute the hash - try (final DigestInputStream stream = new DigestInputStream(connection.getInputStream(), digest); final ReadableByteChannel in = Channels - .newChannel(stream); final FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } - - final String fileSha1 = InstallerUtils.toHexString(digest.digest()); - - if (expected.equalsIgnoreCase(fileSha1)) { - InstallerUtils.LOGGER.info("Successfully downloaded {} and verified checksum!", name); - } else { - Files.delete(path); - throw new IOException(String.format("Checksum verification failed: Expected '%s', got '%s'.", expected, fileSha1)); - } - } -} diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/LibraryManager.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/LibraryManager.java deleted file mode 100644 index 873aeb7ca8f..00000000000 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/LibraryManager.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.forge.applaunch.loading.moddiscovery.library; - -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.spongepowered.forge.applaunch.loading.moddiscovery.library.model.sponge.Libraries; -import org.spongepowered.forge.applaunch.loading.moddiscovery.library.model.sponge.SonatypeResponse; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -// copied from SV -public final class LibraryManager { - - public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?md5=%s&maven" - + ".groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; - private static final Logger LOGGER = LogManager.getLogger(); - - private final boolean checkLibraryHashes; - private final Path rootDirectory; - private final URL librariesUrl; - private final Map libraries; - private final ExecutorService preparationWorker; - - public LibraryManager(final boolean checkLibraryHashes, final Path rootDirectory, final URL librariesUrl) { - this.checkLibraryHashes = checkLibraryHashes; - this.rootDirectory = Objects.requireNonNull(rootDirectory, "rootDirectory"); - this.librariesUrl = Objects.requireNonNull(librariesUrl, "librariesUrl"); - - this.libraries = new LinkedHashMap<>(); - final int availableCpus = Runtime.getRuntime().availableProcessors(); - // We'll be performing mostly IO-blocking operations, so more threads will help us for now - // It might make sense to make this overridable eventually - this.preparationWorker = new ThreadPoolExecutor( - Math.min(Math.max(4, availableCpus * 2), 64), Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>() // this is the number of tasks allowed to be waiting before the pool will spawn off a new thread (unbounded) - ); - } - - public Path getRootDirectory() { - return this.rootDirectory; - } - - public Map getAll() { - return Collections.unmodifiableMap(this.libraries); - } - - protected void addLibrary(final Library library) { - this.libraries.put(library.getName(), library); - } - - public void validate() throws Exception { - LibraryManager.LOGGER.info("Scanning and verifying libraries in '{}'. Please wait, this may take a moment...", this.rootDirectory); - - final Gson gson = new Gson(); - - final Libraries dependencies; - try (final JsonReader reader = new JsonReader(new InputStreamReader(this.librariesUrl.openStream(), StandardCharsets.UTF_8))) { - dependencies = gson.fromJson(reader, Libraries.class); - } - - final Set downloadedDeps = ConcurrentHashMap.newKeySet(); - final List> operations = new ArrayList<>(dependencies.dependencies.size()); - final Set failures = ConcurrentHashMap.newKeySet(); - - for (final Libraries.Dependency dependency : dependencies.dependencies.get("main")) { // todo: evaluate dep sections for forge - operations.add(AsyncUtils.asyncFailableFuture(() -> { - final String groupPath = dependency.group.replace(".", "/"); - final Path depDirectory = - this.rootDirectory.resolve(groupPath).resolve(dependency.module).resolve(dependency.version); - Files.createDirectories(depDirectory); - final Path depFile = depDirectory.resolve(dependency.module + "-" + dependency.version + ".jar"); - final MessageDigest md5 = MessageDigest.getInstance("MD5"); - - final boolean checkHashes = this.checkLibraryHashes; - - if (Files.exists(depFile)) { - if (!checkHashes) { - LibraryManager.LOGGER.info("Detected existing '{}', skipping hash checks...", depFile); - downloadedDeps.add(new Library(dependency.group + "-" + dependency.module, depFile)); - return null; - } - - // Pipe the download stream into the file and compute the SHA-1 - final byte[] bytes = Files.readAllBytes(depFile); - final String fileMd5 = InstallerUtils.toHexString(md5.digest(bytes)); - - if (dependency.md5.equals(fileMd5)) { - LibraryManager.LOGGER.debug("'{}' verified!", depFile); - } else { - LibraryManager.LOGGER.error("Checksum verification failed: Expected {}, {}. Deleting cached '{}'...", - dependency.md5, fileMd5, depFile); - Files.delete(depFile); - - final SonatypeResponse response = this.getResponseFor(gson, dependency); - - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; - - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5, true); - } - } else { - final SonatypeResponse response = this.getResponseFor(gson, dependency); - - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } - - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; - - if (checkHashes) { - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5, true); - } else { - InstallerUtils.download(url, depFile, true); - } - } - - downloadedDeps.add(new Library(dependency.group + "-" + dependency.module, depFile)); - return null; - }, this.preparationWorker)); - } - - - CompletableFuture.allOf(operations.toArray(new CompletableFuture[0])).handle((result, err) -> { - if (err != null) { - failures.add(err.getMessage()); - LibraryManager.LOGGER.error("Failed to download library", err); - } - return result; - }).join(); - - if (!failures.isEmpty()) { - LibraryManager.LOGGER.error("Failed to download some libraries:"); - for (final String message : failures) { - LibraryManager.LOGGER.error(message); - } - System.exit(-1); - } - - for (final Library library : downloadedDeps) { - this.libraries.put(library.getName(), library); - } - } - - private SonatypeResponse getResponseFor(final Gson gson, final Libraries.Dependency dependency) throws IOException { - final URL requestUrl = new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, - dependency.module, dependency.version)); - - final HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - try (final JsonReader reader = new JsonReader(new InputStreamReader(connection.getInputStream()))) { - return gson.fromJson(reader, SonatypeResponse.class); - } - } - - public ExecutorService preparationWorker() { - return this.preparationWorker; - } - - public void finishedProcessing() { - if (this.preparationWorker.isTerminated()) { - return; - } - - this.preparationWorker.shutdown(); - boolean successful; - try { - successful = this.preparationWorker.awaitTermination(10L, TimeUnit.SECONDS); - } catch (final InterruptedException e) { - successful = false; - } - - if (!successful) { - LibraryManager.LOGGER.warn("Failed to shut down library preparation pool in 10 seconds, forcing shutdown now."); - this.preparationWorker.shutdownNow(); - } - } - - public static class Library { - - private final String name; - private final Path file; - - public Library(final String name, final Path file) { - this.name = name; - this.file = file; - } - - public String getName() { - return this.name; - } - - public Path getFile() { - return this.file; - } - } -} diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/AsyncUtils.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/Log4JLogger.java similarity index 57% rename from forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/AsyncUtils.java rename to forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/Log4JLogger.java index 61f212cf090..31f7369862f 100644 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/AsyncUtils.java +++ b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/Log4JLogger.java @@ -24,29 +24,31 @@ */ package org.spongepowered.forge.applaunch.loading.moddiscovery.library; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; +import org.spongepowered.libs.Logger; -final class AsyncUtils { +public final class Log4JLogger implements Logger { + private final org.apache.logging.log4j.Logger delegate; - private AsyncUtils() { + public Log4JLogger(final org.apache.logging.log4j.Logger delegate) { + this.delegate = delegate; } - static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { - final CompletableFuture future = new CompletableFuture<>(); - executor.execute(() -> { - try { - future.complete(action.call()); - } catch (final Exception ex) { - future.completeExceptionally(ex); - } - }); - return future; + @Override + public void log(final Level level, final String message, final Object... args) { + this.delegate.log(this.convertLevel(level), message, args); } - @SuppressWarnings("unchecked") - static R sneakyThrow(final Throwable original) throws T { - throw (T) original; + @Override + public void log(final Level level, final String message, final Throwable throwable) { + this.delegate.log(this.convertLevel(level), message, throwable); + } + + private org.apache.logging.log4j.Level convertLevel(final Level level) { + return switch (level) { + case DEBUG -> org.apache.logging.log4j.Level.DEBUG; + case INFO -> org.apache.logging.log4j.Level.INFO; + case WARN -> org.apache.logging.log4j.Level.WARN; + case ERROR -> org.apache.logging.log4j.Level.ERROR; + }; } } diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java deleted file mode 100644 index f4fbc73b538..00000000000 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.forge.applaunch.loading.moddiscovery.library.model.sponge; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public final class Libraries { - - public Map> dependencies; - - public static final class Dependency { - - public String group, module, version, md5; - - @Override - public int hashCode() { - return Objects.hash(this.group, this.module); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final Dependency that = (Dependency) o; - return this.group.equals(that.group) && - this.module.equals(that.module); - } - } -} diff --git a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java b/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java deleted file mode 100644 index 8ea4e7ce392..00000000000 --- a/forge/src/applaunch/java/org/spongepowered/forge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.forge.applaunch.loading.moddiscovery.library.model.sponge; - -import java.net.URL; -import java.util.List; - -public final class SonatypeResponse { - - public List items; - public String continuationToken; - - public static final class Item { - - public URL downloadUrl; - public String path, id, repository, format; - public Checksum checksum; - } - - public static final class Checksum { - - public String sha1, md5; - } -} diff --git a/library-manager/build.gradle.kts b/library-manager/build.gradle.kts new file mode 100644 index 00000000000..4e7759a828a --- /dev/null +++ b/library-manager/build.gradle.kts @@ -0,0 +1,14 @@ +val organization: String by project +val projectUrl: String by project + +indraSpotlessLicenser { + licenseHeaderFile(rootProject.file("HEADER.txt")) + + property("name", "Sponge") + property("organization", organization) + property("url", projectUrl) +} + +dependencies { + implementation(apiLibs.gson) +} diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/AsyncUtils.java b/library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java similarity index 86% rename from vanilla/src/installer/java/org/spongepowered/vanilla/installer/AsyncUtils.java rename to library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java index 5b1fe2b1ad2..7395fb1e7d9 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/AsyncUtils.java +++ b/library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java @@ -22,18 +22,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.vanilla.installer; +package org.spongepowered.libs; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -final class AsyncUtils { +public final class AsyncUtils { private AsyncUtils() { } - static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { + public static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { final CompletableFuture future = new CompletableFuture<>(); executor.execute(() -> { try { @@ -46,7 +46,7 @@ static CompletableFuture asyncFailableFuture(final Callable action, fi } @SuppressWarnings("unchecked") - static R sneakyThrow(final Throwable original) throws T { + public static R sneakyThrow(final Throwable original) throws T { throw (T) original; } } diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerUtils.java b/library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java similarity index 89% rename from vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerUtils.java rename to library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java index 45820a396a0..b7088272305 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerUtils.java +++ b/library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java @@ -22,9 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.vanilla.installer; - -import org.tinylog.Logger; +package org.spongepowered.libs; import java.io.IOException; import java.io.InputStream; @@ -113,13 +111,13 @@ public static boolean validateSha256(final String expectedHash, final InputStrea * @param path The local path * @throws IOException If there is a problem while downloading the file */ - public static void download(final URL url, final Path path, final boolean requiresRequest) throws IOException { + public static void download(final Logger logger, final URL url, final Path path, final boolean requiresRequest) throws IOException { Files.createDirectories(path.getParent()); final String name = path.getFileName().toString(); - Logger.info("Downloading {}. This may take a while...", name); - Logger.info("URL -> <{}>", url); + logger.info("Downloading {}. This may take a while...", name); + logger.debug("URL -> <{}>", url); if (!requiresRequest) { try (final ReadableByteChannel in = Channels.newChannel(url.openStream()); final FileChannel out = FileChannel.open(path, @@ -149,11 +147,11 @@ public static void download(final URL url, final Path path, final boolean requir * @param expected The SHA-1 expected digest * @throws IOException If there is a problem while downloading the file */ - public static void downloadCheckHash(final URL url, final Path path, final MessageDigest digest, final String expected) throws IOException { + public static void downloadCheckHash(final Logger logger, final URL url, final Path path, final MessageDigest digest, final String expected) throws IOException { final String name = path.getFileName().toString(); - Logger.info("Downloading {}. This may take a while...", name); - Logger.debug("URL -> <{}>", url); + logger.info("Downloading {}. This may take a while...", name); + logger.debug("URL -> <{}>", url); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); @@ -163,7 +161,7 @@ public static void downloadCheckHash(final URL url, final Path path, final Messa connection.connect(); try (final InputStream is = connection.getInputStream()) { - InstallerUtils.transferCheckHash(is, path, digest, expected); + InstallerUtils.transferCheckHash(logger, is, path, digest, expected); } } @@ -175,7 +173,7 @@ public static void downloadCheckHash(final URL url, final Path path, final Messa * @param expected The SHA-1 expected digest * @throws IOException If there is a problem while downloading the file */ - public static void transferCheckHash(final InputStream source, final Path path, final MessageDigest digest, final String expected) throws IOException { + public static void transferCheckHash(final Logger logger, final InputStream source, final Path path, final MessageDigest digest, final String expected) throws IOException { Files.createDirectories(path.getParent()); final String name = path.getFileName().toString(); @@ -189,7 +187,7 @@ public static void transferCheckHash(final InputStream source, final Path path, final String fileSha1 = InstallerUtils.toHexString(digest.digest()); if (expected.equalsIgnoreCase(fileSha1)) { - Logger.info("Successfully processed {} and verified checksum!", name); + logger.info("Successfully processed {} and verified checksum!", name); } else { Files.delete(path); throw new IOException(String.format("Checksum verification for %s failed: Expected '%s', got '%s'.", name, expected, fileSha1)); diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java similarity index 79% rename from vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java rename to library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java index 8c939f4c4e9..3aad0ec968a 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java @@ -22,14 +22,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.vanilla.installer; +package org.spongepowered.libs; import com.google.gson.Gson; import com.google.gson.stream.JsonReader; -import org.spongepowered.vanilla.installer.model.sponge.Libraries; -import org.spongepowered.vanilla.installer.model.sponge.Libraries.Dependency; -import org.spongepowered.vanilla.installer.model.sponge.SonatypeResponse; -import org.tinylog.Logger; +import org.spongepowered.libs.model.Libraries; +import org.spongepowered.libs.model.SonatypeResponse; import java.io.IOException; import java.io.InputStreamReader; @@ -39,13 +37,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -54,7 +46,10 @@ import java.util.concurrent.TimeUnit; public final class LibraryManager { + public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?md5=%s&maven" + + ".groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; + private final Logger logger; private final boolean checkLibraryHashes; private final Path rootDirectory; private final URL librariesUrl; @@ -62,10 +57,11 @@ public final class LibraryManager { private final ExecutorService preparationWorker; private final Gson gson; - public LibraryManager(final boolean checkLibraryHashes, final Path rootDirectory, final URL librariesUrl) { + public LibraryManager(final Logger logger, final boolean checkLibraryHashes, final Path rootDirectory, final URL librariesUrl) { + this.logger = Objects.requireNonNull(logger, "logger"); this.checkLibraryHashes = checkLibraryHashes; - this.rootDirectory = rootDirectory; - this.librariesUrl = librariesUrl; + this.rootDirectory = Objects.requireNonNull(rootDirectory, "rootDirectory"); + this.librariesUrl = Objects.requireNonNull(librariesUrl, "librariesUrl"); this.libraries = new LinkedHashMap<>(); final int availableCpus = Runtime.getRuntime().availableProcessors(); @@ -87,13 +83,12 @@ public Set getAll(final String collection) { return Collections.unmodifiableSet(this.libraries.getOrDefault(collection, Collections.emptySet())); } - void addLibrary(final String set, final Library library) { + public void addLibrary(final String set, final Library library) { this.libraries.computeIfAbsent(set, $ -> Collections.synchronizedSet(new LinkedHashSet<>())).add(library); } public void validate() throws Exception { - Logger.info("Scanning and verifying libraries in '{}'. Please wait, this may take a moment...", - LauncherCommandLine.librariesDirectory.toAbsolutePath()); + this.logger.info("Scanning and verifying libraries in '{}'. Please wait, this may take a moment...", this.rootDirectory); final Libraries dependencies; try (final JsonReader reader = new JsonReader(new InputStreamReader(this.librariesUrl.openStream(), StandardCharsets.UTF_8))) { @@ -104,22 +99,22 @@ public void validate() throws Exception { final Map> operations = new HashMap<>(); final Set failures = ConcurrentHashMap.newKeySet(); - for (final Map.Entry> setEntry : dependencies.dependencies.entrySet()) { + for (final Map.Entry> setEntry : dependencies.dependencies.entrySet()) { downloadedDeps.put(setEntry.getKey(), this.scheduleDownloads(setEntry.getKey(), setEntry.getValue(), operations, failures)); } CompletableFuture.allOf(operations.values().toArray(new CompletableFuture[0])).handle((result, err) -> { if (err != null) { failures.add(err.getMessage()); - Logger.error(err, "Failed to download library"); + this.logger.error("Failed to download library", err); } return result; }).join(); if (!failures.isEmpty()) { - Logger.error("Failed to download some libraries:"); + this.logger.error("Failed to download some libraries:"); for (final String message : failures) { - Logger.error(message); + this.logger.error(message); } System.exit(-1); } @@ -129,7 +124,7 @@ public void validate() throws Exception { private Set scheduleDownloads( final String collection, - final List dependencies, + final List dependencies, final Map> operations, final Set failures ) { @@ -147,7 +142,7 @@ private Set scheduleDownloads( if (Files.exists(depFile)) { if (!checkHashes) { - Logger.info("Detected existing '{}', skipping hash checks...", depFile); + this.logger.info("Detected existing '{}', skipping hash checks...", depFile); return depFile; } @@ -156,16 +151,16 @@ private Set scheduleDownloads( final String fileMd5 = InstallerUtils.toHexString(md5.digest(bytes)); if (dependency.md5.equals(fileMd5)) { - Logger.debug("'{}' verified!", depFile); + this.logger.debug("'{}' verified!", depFile); } else { - Logger.error("Checksum verification failed: Expected {}, {}. Deleting cached '{}'...", + this.logger.error("Checksum verification failed: Expected {}, {}. Deleting cached '{}'...", dependency.md5, fileMd5, depFile); Files.delete(depFile); final SonatypeResponse response = this.getResponseFor(this.gson, dependency); if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(Constants.Libraries.SPONGE_NEXUS_DOWNLOAD_URL, + failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, dependency.module, dependency.version)) + "'!"); return null; @@ -173,13 +168,13 @@ private Set scheduleDownloads( final SonatypeResponse.Item item = response.items.get(0); final URL url = item.downloadUrl; - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5); + InstallerUtils.downloadCheckHash(this.logger, url, depFile, md5, item.checksum.md5); } } else { final SonatypeResponse response = this.getResponseFor(this.gson, dependency); if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(Constants.Libraries.SPONGE_NEXUS_DOWNLOAD_URL, + failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, dependency.module, dependency.version)) + "'!"); return null; @@ -189,9 +184,9 @@ private Set scheduleDownloads( final URL url = item.downloadUrl; if (checkHashes) { - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5); + InstallerUtils.downloadCheckHash(this.logger, url, depFile, md5, item.checksum.md5); } else { - InstallerUtils.download(url, depFile, true); + InstallerUtils.download(this.logger, url, depFile, true); } } @@ -206,13 +201,13 @@ private Set scheduleDownloads( } private SonatypeResponse getResponseFor(final Gson gson, final Libraries.Dependency dependency) throws IOException { - final URL requestUrl = new URL(String.format(Constants.Libraries.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, + final URL requestUrl = new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, dependency.module, dependency.version)); final HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader Minecraft/" + Constants.Libraries.MINECRAFT_VERSION_TARGET); + connection.setRequestProperty("User-Agent", "Sponge-Downloader"); connection.connect(); @@ -239,14 +234,14 @@ public void finishedProcessing() { } if (!successful) { - Logger.warn("Failed to shut down library preparation pool in 10 seconds, forcing shutdown now."); + this.logger.warn("Failed to shut down library preparation pool in 10 seconds, forcing shutdown now."); this.preparationWorker.shutdownNow(); } } public record Library(String name, Path file) {} - private static String asId(final Dependency dep) { + private static String asId(final Libraries.Dependency dep) { return dep.group + ':' + dep.module + ':' + dep.version; } } diff --git a/library-manager/src/main/java/org/spongepowered/libs/Logger.java b/library-manager/src/main/java/org/spongepowered/libs/Logger.java new file mode 100644 index 00000000000..79d52a6b169 --- /dev/null +++ b/library-manager/src/main/java/org/spongepowered/libs/Logger.java @@ -0,0 +1,69 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.libs; + +// Common interface for log4j and tinylog +public interface Logger { + + enum Level { + DEBUG, INFO, WARN, ERROR + } + + default void debug(final String message, final Object... args) { + this.log(Level.DEBUG, message, args); + } + + default void debug(final Level level, final String message, final Throwable throwable) { + this.log(Level.DEBUG, message, throwable); + } + + default void info(final String message, final Object... args) { + this.log(Level.INFO, message, args); + } + + default void info(final Level level, final String message, final Throwable throwable) { + this.log(Level.INFO, message, throwable); + } + + default void warn(final String message, final Object... args) { + this.log(Level.WARN, message, args); + } + + default void warn(final Level level, final String message, final Throwable throwable) { + this.log(Level.WARN, message, throwable); + } + + default void error(final String message, final Object... args) { + this.log(Level.ERROR, message, args); + } + + default void error(final Level level, final String message, final Throwable throwable) { + this.log(Level.ERROR, message, throwable); + } + + void log(final Level level, final String message, final Object... args); + + void log(final Level level, final String message, final Throwable throwable); +} diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/Libraries.java b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java similarity index 97% rename from vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/Libraries.java rename to library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java index 056133bec00..905bfff8d5e 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/Libraries.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.vanilla.installer.model.sponge; +package org.spongepowered.libs.model; import java.util.List; import java.util.Map; diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/SonatypeResponse.java b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java similarity index 96% rename from vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/SonatypeResponse.java rename to library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java index 8a1a8d97950..7cd40d7b79c 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/sponge/SonatypeResponse.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.vanilla.installer.model.sponge; +package org.spongepowered.libs.model; import java.net.URL; import java.util.List; diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index e6cbbac35a7..f0cbddbc02c 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -18,7 +18,8 @@ plugins { } val commonProject = parent!! -val transformersProject = parent!!.project(":modlauncher-transformers") +val transformersProject = commonProject.project(":modlauncher-transformers") +val libraryManagerProject = commonProject.project(":library-manager") val testPluginsProject: Project? = rootProject.subprojects.find { "testplugins" == it.name } val apiVersion: String by project @@ -198,6 +199,7 @@ dependencies { service(project(transformersProject.path)) { exclude(group = "cpw.mods", module = "modlauncher") } + service(project(libraryManagerProject.path)) service(platform(apiLibs.configurate.bom)) service(apiLibs.configurate.core) { exclude(group = "org.checkerframework", module = "checker-qual") @@ -219,6 +221,7 @@ dependencies { val serviceShadedLibraries = serviceShadedLibrariesConfig.name serviceShadedLibraries(project(transformersProject.path)) { isTransitive = false } + serviceShadedLibraries(project(libraryManagerProject.path)) { isTransitive = false } val gameShadedLibraries = gameShadedLibrariesConfig.name gameShadedLibraries("org.spongepowered:spongeapi:$apiVersion") { isTransitive = false } diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/SpongeNeoDependencyLocator.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/SpongeNeoDependencyLocator.java index d335561d374..eb56caf7f1e 100644 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/SpongeNeoDependencyLocator.java +++ b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/SpongeNeoDependencyLocator.java @@ -35,7 +35,8 @@ import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.LibraryManager; +import org.spongepowered.libs.LibraryManager; +import org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.Log4JLogger; import org.spongepowered.neoforge.applaunch.transformation.SpongeNeoTransformationService; import java.nio.file.Path; @@ -52,6 +53,7 @@ public void scanMods(final List loadedMods, final IDiscoveryPipeline p final Environment env = Launcher.INSTANCE.environment(); LibraryManager libraryManager = new LibraryManager( + new Log4JLogger(LogManager.getLogger(LibraryManager.class)), env.getProperty(SpongeNeoTransformationService.Keys.CHECK_LIBRARY_HASHES.get()).orElse(true), env.getProperty(SpongeNeoTransformationService.Keys.LIBRARIES_DIRECTORY.get()) .orElseThrow(() -> new IllegalStateException("no libraries available")), @@ -66,8 +68,8 @@ public void scanMods(final List loadedMods, final IDiscoveryPipeline p libraryManager.finishedProcessing(); final ModFileDiscoveryAttributes attributes = ModFileDiscoveryAttributes.DEFAULT.withDependencyLocator(this); - for (final LibraryManager.Library library : libraryManager.getAll().values()) { - final Path path = library.getFile(); + for (final LibraryManager.Library library : libraryManager.getAll("main")) { + final Path path = library.file(); SpongeNeoDependencyLocator.LOGGER.debug("Proposing jar {} as a game library", path); pipeline.addModFile(IModFile.create(SecureJar.from(path), JarModsDotTomlModFileReader::manifestParser, IModFile.Type.GAMELIBRARY, attributes)); diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/InstallerUtils.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/InstallerUtils.java deleted file mode 100644 index 373c3c09bbe..00000000000 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/InstallerUtils.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.neoforge.applaunch.loading.moddiscovery.library; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -final class InstallerUtils { - - private static final Logger LOGGER = LogManager.getLogger(); - // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java - private static final char[] hexArray = "0123456789abcdef".toCharArray(); - - private InstallerUtils() { - } - - public static String toHexString(final byte[] bytes) { - final char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - final int v = bytes[j] & 0xFF; - hexChars[j * 2] = InstallerUtils.hexArray[v >>> 4]; - hexChars[j * 2 + 1] = InstallerUtils.hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - public static boolean validateSha1(final String expectedHash, final Path path) throws IOException { - try (final InputStream is = Files.newInputStream(path)) { - return InstallerUtils.validateSha1(expectedHash, is); - } - } - - public static boolean validateSha1(final String expectedHash, final InputStream stream) throws IOException { - final MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-1"); - } catch (final NoSuchAlgorithmException ex) { - throw new AssertionError(ex); // Guaranteed present by MessageDigest spec - } - - final byte[] buf = new byte[4096]; - int read; - - while ((read = stream.read(buf)) != -1) { - digest.update(buf,0, read); - } - - return expectedHash.equals(InstallerUtils.toHexString(digest.digest())); - } - - /** - * Downloads a file. - * - * @param url The file URL - * @param path The local path - * @throws IOException If there is a problem while downloading the file - */ - public static void download(final URL url, final Path path, final boolean requiresRequest) throws IOException { - Files.createDirectories(path.getParent()); - - final String name = path.getFileName().toString(); - - InstallerUtils.LOGGER.info("Downloading {}. This may take a while...", name); - InstallerUtils.LOGGER.info("URL -> <{}>", url); - - if (!requiresRequest) { - try (final ReadableByteChannel in = Channels.newChannel(url.openStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } else { - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - try (final ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } - } - - /** - * Downloads a file and verify its digest. - * - * @param url The file URL - * @param path The local path - * @param expected The SHA-1 expected digest - * @throws IOException If there is a problem while downloading the file - */ - public static void downloadCheckHash(final URL url, final Path path, final MessageDigest digest, final String expected, - final boolean requiresRequest) throws IOException { - Files.createDirectories(path.getParent()); - - final String name = path.getFileName().toString(); - - InstallerUtils.LOGGER.info("Downloading {}. This may take a while...", name); - InstallerUtils.LOGGER.debug("URL -> <{}>", url); - - if (!requiresRequest) { - // Pipe the download stream into the file and compute the hash - try (final DigestInputStream stream = new DigestInputStream(url.openStream(), digest); final ReadableByteChannel in = Channels - .newChannel(stream); final FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } else { - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - // Pipe the download stream into the file and compute the hash - try (final DigestInputStream stream = new DigestInputStream(connection.getInputStream(), digest); final ReadableByteChannel in = Channels - .newChannel(stream); final FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - out.transferFrom(in, 0, Long.MAX_VALUE); - } - } - - final String fileSha1 = InstallerUtils.toHexString(digest.digest()); - - if (expected.equalsIgnoreCase(fileSha1)) { - InstallerUtils.LOGGER.info("Successfully downloaded {} and verified checksum!", name); - } else { - Files.delete(path); - throw new IOException(String.format("Checksum verification failed: Expected '%s', got '%s'.", expected, fileSha1)); - } - } -} diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/LibraryManager.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/LibraryManager.java deleted file mode 100644 index fd730afe55b..00000000000 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/LibraryManager.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.neoforge.applaunch.loading.moddiscovery.library; - -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.model.sponge.Libraries; -import org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.model.sponge.SonatypeResponse; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -// copied from SV -public final class LibraryManager { - - public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?md5=%s&maven" - + ".groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; - private static final Logger LOGGER = LogManager.getLogger(); - - private final boolean checkLibraryHashes; - private final Path rootDirectory; - private final URL librariesUrl; - private final Map libraries; - private final ExecutorService preparationWorker; - - public LibraryManager(final boolean checkLibraryHashes, final Path rootDirectory, final URL librariesUrl) { - this.checkLibraryHashes = checkLibraryHashes; - this.rootDirectory = Objects.requireNonNull(rootDirectory, "rootDirectory"); - this.librariesUrl = Objects.requireNonNull(librariesUrl, "librariesUrl"); - - this.libraries = new LinkedHashMap<>(); - final int availableCpus = Runtime.getRuntime().availableProcessors(); - // We'll be performing mostly IO-blocking operations, so more threads will help us for now - // It might make sense to make this overridable eventually - this.preparationWorker = new ThreadPoolExecutor( - Math.min(Math.max(4, availableCpus * 2), 64), Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>() // this is the number of tasks allowed to be waiting before the pool will spawn off a new thread (unbounded) - ); - } - - public Path getRootDirectory() { - return this.rootDirectory; - } - - public Map getAll() { - return Collections.unmodifiableMap(this.libraries); - } - - protected void addLibrary(final Library library) { - this.libraries.put(library.getName(), library); - } - - public void validate() throws Exception { - LibraryManager.LOGGER.info("Scanning and verifying libraries in '{}'. Please wait, this may take a moment...", this.rootDirectory); - - final Gson gson = new Gson(); - - final Libraries dependencies; - try (final JsonReader reader = new JsonReader(new InputStreamReader(this.librariesUrl.openStream(), StandardCharsets.UTF_8))) { - dependencies = gson.fromJson(reader, Libraries.class); - } - - final Set downloadedDeps = ConcurrentHashMap.newKeySet(); - final List> operations = new ArrayList<>(dependencies.dependencies.size()); - final Set failures = ConcurrentHashMap.newKeySet(); - - for (final Libraries.Dependency dependency : dependencies.dependencies.get("main")) { // todo: evaluate dep sections for forge - operations.add(AsyncUtils.asyncFailableFuture(() -> { - final String groupPath = dependency.group.replace(".", "/"); - final Path depDirectory = - this.rootDirectory.resolve(groupPath).resolve(dependency.module).resolve(dependency.version); - Files.createDirectories(depDirectory); - final Path depFile = depDirectory.resolve(dependency.module + "-" + dependency.version + ".jar"); - final MessageDigest md5 = MessageDigest.getInstance("MD5"); - - final boolean checkHashes = this.checkLibraryHashes; - - if (Files.exists(depFile)) { - if (!checkHashes) { - LibraryManager.LOGGER.info("Detected existing '{}', skipping hash checks...", depFile); - downloadedDeps.add(new Library(dependency.group + "-" + dependency.module, depFile)); - return null; - } - - // Pipe the download stream into the file and compute the SHA-1 - final byte[] bytes = Files.readAllBytes(depFile); - final String fileMd5 = InstallerUtils.toHexString(md5.digest(bytes)); - - if (dependency.md5.equals(fileMd5)) { - LibraryManager.LOGGER.debug("'{}' verified!", depFile); - } else { - LibraryManager.LOGGER.error("Checksum verification failed: Expected {}, {}. Deleting cached '{}'...", - dependency.md5, fileMd5, depFile); - Files.delete(depFile); - - final SonatypeResponse response = this.getResponseFor(gson, dependency); - - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; - - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5, true); - } - } else { - final SonatypeResponse response = this.getResponseFor(gson, dependency); - - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } - - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; - - if (checkHashes) { - InstallerUtils.downloadCheckHash(url, depFile, md5, item.checksum.md5, true); - } else { - InstallerUtils.download(url, depFile, true); - } - } - - downloadedDeps.add(new Library(dependency.group + "-" + dependency.module, depFile)); - return null; - }, this.preparationWorker)); - } - - - CompletableFuture.allOf(operations.toArray(new CompletableFuture[0])).handle((result, err) -> { - if (err != null) { - failures.add(err.getMessage()); - LibraryManager.LOGGER.error("Failed to download library", err); - } - return result; - }).join(); - - if (!failures.isEmpty()) { - LibraryManager.LOGGER.error("Failed to download some libraries:"); - for (final String message : failures) { - LibraryManager.LOGGER.error(message); - } - System.exit(-1); - } - - for (final Library library : downloadedDeps) { - this.libraries.put(library.getName(), library); - } - } - - private SonatypeResponse getResponseFor(final Gson gson, final Libraries.Dependency dependency) throws IOException { - final URL requestUrl = new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, - dependency.module, dependency.version)); - - final HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Sponge-Downloader"); - - connection.connect(); - - try (final JsonReader reader = new JsonReader(new InputStreamReader(connection.getInputStream()))) { - return gson.fromJson(reader, SonatypeResponse.class); - } - } - - public ExecutorService preparationWorker() { - return this.preparationWorker; - } - - public void finishedProcessing() { - if (this.preparationWorker.isTerminated()) { - return; - } - - this.preparationWorker.shutdown(); - boolean successful; - try { - successful = this.preparationWorker.awaitTermination(10L, TimeUnit.SECONDS); - } catch (final InterruptedException e) { - successful = false; - } - - if (!successful) { - LibraryManager.LOGGER.warn("Failed to shut down library preparation pool in 10 seconds, forcing shutdown now."); - this.preparationWorker.shutdownNow(); - } - } - - public static class Library { - - private final String name; - private final Path file; - - public Library(final String name, final Path file) { - this.name = name; - this.file = file; - } - - public String getName() { - return this.name; - } - - public Path getFile() { - return this.file; - } - } -} diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/AsyncUtils.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/Log4JLogger.java similarity index 57% rename from neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/AsyncUtils.java rename to neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/Log4JLogger.java index 042960a8ab1..efac9b34303 100644 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/AsyncUtils.java +++ b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/Log4JLogger.java @@ -24,29 +24,31 @@ */ package org.spongepowered.neoforge.applaunch.loading.moddiscovery.library; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; +import org.spongepowered.libs.Logger; -final class AsyncUtils { +public final class Log4JLogger implements Logger { + private final org.apache.logging.log4j.Logger delegate; - private AsyncUtils() { + public Log4JLogger(final org.apache.logging.log4j.Logger delegate) { + this.delegate = delegate; } - static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { - final CompletableFuture future = new CompletableFuture<>(); - executor.execute(() -> { - try { - future.complete(action.call()); - } catch (final Exception ex) { - future.completeExceptionally(ex); - } - }); - return future; + @Override + public void log(final Level level, final String message, final Object... args) { + this.delegate.log(this.convertLevel(level), message, args); } - @SuppressWarnings("unchecked") - static R sneakyThrow(final Throwable original) throws T { - throw (T) original; + @Override + public void log(final Level level, final String message, final Throwable throwable) { + this.delegate.log(this.convertLevel(level), message, throwable); + } + + private org.apache.logging.log4j.Level convertLevel(final Level level) { + return switch (level) { + case DEBUG -> org.apache.logging.log4j.Level.DEBUG; + case INFO -> org.apache.logging.log4j.Level.INFO; + case WARN -> org.apache.logging.log4j.Level.WARN; + case ERROR -> org.apache.logging.log4j.Level.ERROR; + }; } } diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java deleted file mode 100644 index 025de88a32f..00000000000 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/Libraries.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.model.sponge; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public final class Libraries { - - public Map> dependencies; - - public static final class Dependency { - - public String group, module, version, md5; - - @Override - public int hashCode() { - return Objects.hash(this.group, this.module); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final Dependency that = (Dependency) o; - return this.group.equals(that.group) && - this.module.equals(that.module); - } - } -} diff --git a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java b/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java deleted file mode 100644 index 097d3fd36d3..00000000000 --- a/neoforge/src/applaunch/java/org/spongepowered/neoforge/applaunch/loading/moddiscovery/library/model/sponge/SonatypeResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.neoforge.applaunch.loading.moddiscovery.library.model.sponge; - -import java.net.URL; -import java.util.List; - -public final class SonatypeResponse { - - public List items; - public String continuationToken; - - public static final class Item { - - public URL downloadUrl; - public String path, id, repository, format; - public Checksum checksum; - } - - public static final class Checksum { - - public String sha1, md5; - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a4d40fc5ee..00d638da944 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,6 +69,7 @@ includeBuild("SpongeAPI") { } } include("modlauncher-transformers") +include("library-manager") include("generator") // Optional projects diff --git a/vanilla/build.gradle.kts b/vanilla/build.gradle.kts index d1a735cd007..13c93224650 100644 --- a/vanilla/build.gradle.kts +++ b/vanilla/build.gradle.kts @@ -9,7 +9,8 @@ plugins { } val commonProject = parent!! -val transformersProject = parent!!.project(":modlauncher-transformers") +val transformersProject = commonProject.project(":modlauncher-transformers") +val libraryManagerProject = commonProject.project(":library-manager") val testPluginsProject: Project? = rootProject.subprojects.find { "testplugins" == it.name } val apiVersion: String by project @@ -164,6 +165,8 @@ dependencies { exclude(group = "org.ow2.asm") } + installer(project(libraryManagerProject.path)) + val init = initLibrariesConfig.name init(libs.securemodules) init(libs.asm.commons) diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/Installer.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/Installer.java index 4f52451b5ac..62401bd04fc 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/Installer.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/Installer.java @@ -30,6 +30,8 @@ import org.spongepowered.configurate.loader.ConfigurationLoader; import org.spongepowered.configurate.objectmapping.ObjectMapper; import org.spongepowered.configurate.objectmapping.meta.NodeResolver; +import org.spongepowered.libs.LibraryManager; +import org.spongepowered.vanilla.installer.library.TinyLogger; import java.nio.file.Path; import java.nio.file.Paths; @@ -56,6 +58,7 @@ public Installer(final Path directory) throws ConfigurateException { .build(); this.config = this.loadConfig(); this.libraryManager = new LibraryManager( + TinyLogger.INSTANCE, this.config.checkLibraryHashes, Paths.get(this.config.librariesDirectory.replace("${BASE_DIRECTORY}", directory.toAbsolutePath().toString())), this.getClass().getResource("/libraries.json") diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java index b7220520f38..a6d1f088892 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java @@ -31,6 +31,10 @@ import net.minecraftforge.fart.api.SourceFixerConfig; import net.minecraftforge.fart.api.Transformer; import net.minecraftforge.srgutils.IMappingFile; +import org.spongepowered.libs.AsyncUtils; +import org.spongepowered.libs.InstallerUtils; +import org.spongepowered.libs.LibraryManager; +import org.spongepowered.vanilla.installer.library.TinyLogger; import org.spongepowered.vanilla.installer.model.GroupArtifactVersion; import org.spongepowered.vanilla.installer.model.mojang.BundleElement; import org.spongepowered.vanilla.installer.model.mojang.BundlerMetadata; @@ -302,9 +306,8 @@ private CompletableFuture downloadMinecraft(final Version version, final P throw new IOException( String.format("The Minecraft jar is not located at '%s' and downloading it has been turned off.", downloadTarget)); } - InstallerUtils - .downloadCheckHash(version.downloads.server.url, downloadTarget, MessageDigest.getInstance("SHA-1"), - version.downloads.server.sha1); + InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, + MessageDigest.getInstance("SHA-1"), version.downloads.server.sha1); } else { if (this.installer.getLauncherConfig().checkLibraryHashes) { Logger.info("Detected existing Minecraft Server jar, verifying hashes..."); @@ -316,8 +319,8 @@ private CompletableFuture downloadMinecraft(final Version version, final P Logger.error("Checksum verification failed: Expected {}. Deleting cached Minecraft Server jar...", version.downloads.server.sha1); Files.delete(downloadTarget); - InstallerUtils.downloadCheckHash(version.downloads.server.url, downloadTarget, - MessageDigest.getInstance("SHA-1"), version.downloads.server.sha1); + InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, + MessageDigest.getInstance("SHA-1"), version.downloads.server.sha1); } } else { Logger.info("Detected existing Minecraft jar. Skipping hash check as that is turned off..."); @@ -352,7 +355,7 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar if (serverExtractionNeeded) { final ZipEntry serverEntry = bundle.getEntry(server.path()); try (final InputStream is = bundle.getInputStream(serverEntry)) { - InstallerUtils.transferCheckHash(is, serverDestination, MessageDigest.getInstance("SHA-256"), server.sha256()); + InstallerUtils.transferCheckHash(TinyLogger.INSTANCE, is, serverDestination, MessageDigest.getInstance("SHA-256"), server.sha256()); } } @@ -372,7 +375,7 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar final ZipEntry entry = bundle.getEntry(library.path()); try (final InputStream is = bundle.getInputStream(entry)) { - InstallerUtils.transferCheckHash(is, destination, MessageDigest.getInstance("SHA-256"), library.sha256()); + InstallerUtils.transferCheckHash(TinyLogger.INSTANCE, is, destination, MessageDigest.getInstance("SHA-256"), library.sha256()); libs.put(gav, destination); } } @@ -415,10 +418,10 @@ private CompletableFuture downloadMappings(final Version version, final Pa if (this.installer.getLauncherConfig().autoDownloadLibraries) { if (checkHashes) { - InstallerUtils.downloadCheckHash(mappings.url, downloadTarget, + InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, mappings.url, downloadTarget, MessageDigest.getInstance("SHA-1"), mappings.sha1); } else { - InstallerUtils.download(mappings.url, downloadTarget, false); + InstallerUtils.download(TinyLogger.INSTANCE, mappings.url, downloadTarget, false); } } else { throw new IOException(String.format("Mappings were not located at '%s' and downloading them has been turned off.", downloadTarget)); diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/library/TinyLogger.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/library/TinyLogger.java new file mode 100644 index 00000000000..45fb20a7aa8 --- /dev/null +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/library/TinyLogger.java @@ -0,0 +1,60 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.vanilla.installer.library; + +import org.spongepowered.libs.Logger; +import org.tinylog.configuration.Configuration; +import org.tinylog.format.AdvancedMessageFormatter; +import org.tinylog.format.MessageFormatter; +import org.tinylog.provider.LoggingProvider; +import org.tinylog.provider.ProviderRegistry; + +public final class TinyLogger implements Logger { + private static final MessageFormatter formatter = new AdvancedMessageFormatter(Configuration.getLocale(), Configuration.isEscapingEnabled()); + private static final LoggingProvider provider = ProviderRegistry.getLoggingProvider(); + + public static final TinyLogger INSTANCE = new TinyLogger(); + + private TinyLogger() {} + + @Override + public void log(final Level level, final String message, final Object... args) { + TinyLogger.provider.log(2, null, this.convertLevel(level), null, TinyLogger.formatter, message, args); + } + + @Override + public void log(final Level level, final String message, final Throwable throwable) { + TinyLogger.provider.log(2, null, this.convertLevel(level), throwable, TinyLogger.formatter, message, (Object[]) null); + } + + private org.tinylog.Level convertLevel(final Level level) { + return switch (level) { + case DEBUG -> org.tinylog.Level.DEBUG; + case INFO -> org.tinylog.Level.INFO; + case WARN -> org.tinylog.Level.WARN; + case ERROR -> org.tinylog.Level.ERROR; + }; + } +} From 20778e8c4fc4b4f6be0023428bb14cab7591976c Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Tue, 19 Nov 2024 21:55:54 +0100 Subject: [PATCH 21/24] Cleanup LibraryManager --- .../org/spongepowered/libs/AsyncUtils.java | 52 ------- .../spongepowered/libs/LibraryManager.java | 96 ++++++------- ...{InstallerUtils.java => LibraryUtils.java} | 127 ++++++++---------- .../spongepowered/libs/model/Libraries.java | 29 +--- .../libs/model/SonatypeResponse.java | 19 +-- .../vanilla/installer/Constants.java | 3 - .../vanilla/installer/InstallerMain.java | 59 ++++---- 7 files changed, 128 insertions(+), 257 deletions(-) delete mode 100644 library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java rename library-manager/src/main/java/org/spongepowered/libs/{InstallerUtils.java => LibraryUtils.java} (57%) diff --git a/library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java b/library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java deleted file mode 100644 index 7395fb1e7d9..00000000000 --- a/library-manager/src/main/java/org/spongepowered/libs/AsyncUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.libs; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public final class AsyncUtils { - - private AsyncUtils() { - } - - public static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { - final CompletableFuture future = new CompletableFuture<>(); - executor.execute(() -> { - try { - future.complete(action.call()); - } catch (final Exception ex) { - future.completeExceptionally(ex); - } - }); - return future; - } - - @SuppressWarnings("unchecked") - public static R sneakyThrow(final Throwable original) throws T { - throw (T) original; - } -} diff --git a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java index 3aad0ec968a..eb43baf8c78 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java @@ -32,12 +32,19 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -84,7 +91,7 @@ public Set getAll(final String collection) { } public void addLibrary(final String set, final Library library) { - this.libraries.computeIfAbsent(set, $ -> Collections.synchronizedSet(new LinkedHashSet<>())).add(library); + this.libraries.computeIfAbsent(set, key -> Collections.synchronizedSet(new LinkedHashSet<>())).add(library); } public void validate() throws Exception { @@ -99,8 +106,8 @@ public void validate() throws Exception { final Map> operations = new HashMap<>(); final Set failures = ConcurrentHashMap.newKeySet(); - for (final Map.Entry> setEntry : dependencies.dependencies.entrySet()) { - downloadedDeps.put(setEntry.getKey(), this.scheduleDownloads(setEntry.getKey(), setEntry.getValue(), operations, failures)); + for (final Map.Entry> entry : dependencies.dependencies().entrySet()) { + downloadedDeps.put(entry.getKey(), this.scheduleDownloads(entry.getValue(), operations, failures)); } CompletableFuture.allOf(operations.values().toArray(new CompletableFuture[0])).handle((result, err) -> { @@ -123,20 +130,17 @@ public void validate() throws Exception { } private Set scheduleDownloads( - final String collection, final List dependencies, final Map> operations, final Set failures ) { final Set downloadedDeps = Collections.synchronizedSet(new LinkedHashSet<>(dependencies.size())); - for (final Libraries.Dependency dependency : dependencies) { - operations.computeIfAbsent(asId(dependency), $ -> AsyncUtils.asyncFailableFuture(() -> { - final String groupPath = dependency.group.replace(".", "/"); - final Path depDirectory = - this.rootDirectory.resolve(groupPath).resolve(dependency.module).resolve(dependency.version); + for (final Libraries.Dependency dep : dependencies) { + operations.computeIfAbsent(getId(dep), key -> LibraryUtils.asyncFailableFuture(() -> { + final String groupPath = dep.group().replace(".", "/"); + final Path depDirectory = this.rootDirectory.resolve(groupPath).resolve(dep.module()).resolve(dep.version()); Files.createDirectories(depDirectory); - final Path depFile = depDirectory.resolve(dependency.module + "-" + dependency.version + ".jar"); - final MessageDigest md5 = MessageDigest.getInstance("MD5"); + final Path depFile = depDirectory.resolve(dep.module() + "-" + dep.version() + ".jar"); final boolean checkHashes = this.checkLibraryHashes; @@ -146,64 +150,42 @@ private Set scheduleDownloads( return depFile; } - // Pipe the download stream into the file and compute the SHA-1 - final byte[] bytes = Files.readAllBytes(depFile); - final String fileMd5 = InstallerUtils.toHexString(md5.digest(bytes)); - - if (dependency.md5.equals(fileMd5)) { + if (LibraryUtils.validateDigest("MD5", dep.md5(), depFile)) { this.logger.debug("'{}' verified!", depFile); - } else { - this.logger.error("Checksum verification failed: Expected {}, {}. Deleting cached '{}'...", - dependency.md5, fileMd5, depFile); - Files.delete(depFile); - - final SonatypeResponse response = this.getResponseFor(this.gson, dependency); - - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; - - InstallerUtils.downloadCheckHash(this.logger, url, depFile, md5, item.checksum.md5); + return depFile; } - } else { - final SonatypeResponse response = this.getResponseFor(this.gson, dependency); - if (response.items.isEmpty()) { - failures.add("No data received from '" + new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dependency.md5, dependency.group, - dependency.module, dependency.version)) + "'!"); - return null; - } + this.logger.error("Checksum verification failed: Expected {}. Deleting cached '{}'...", dep.md5(), depFile); + Files.delete(depFile); + } - final SonatypeResponse.Item item = response.items.get(0); - final URL url = item.downloadUrl; + final URL requestUrl = URI.create(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, + dep.md5(), dep.group(), dep.module(), dep.version())).toURL(); + final SonatypeResponse response = this.getResponseFor(this.gson, requestUrl); + if (response.items().isEmpty()) { + failures.add("No data received from '" + requestUrl + "'!"); + return null; + } - if (checkHashes) { - InstallerUtils.downloadCheckHash(this.logger, url, depFile, md5, item.checksum.md5); - } else { - InstallerUtils.download(this.logger, url, depFile, true); - } + final SonatypeResponse.Item item = response.items().get(0); + + if (checkHashes) { + LibraryUtils.downloadAndVerifyDigest(this.logger, item.downloadUrl(), depFile, "MD5", item.checksum().md5()); + } else { + LibraryUtils.download(this.logger, item.downloadUrl(), depFile, true); } return depFile; }, this.preparationWorker)).whenComplete((res, err) -> { if (res != null) { - downloadedDeps.add(new Library(asId(dependency), res)); + downloadedDeps.add(new Library(getId(dep), res)); } }); } return downloadedDeps; } - private SonatypeResponse getResponseFor(final Gson gson, final Libraries.Dependency dependency) throws IOException { - final URL requestUrl = new URL(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dependency.md5, dependency.group, - dependency.module, dependency.version)); - + private SonatypeResponse getResponseFor(final Gson gson, final URL requestUrl) throws IOException { final HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Content-Type", "application/json"); @@ -241,7 +223,7 @@ public void finishedProcessing() { public record Library(String name, Path file) {} - private static String asId(final Libraries.Dependency dep) { - return dep.group + ':' + dep.module + ':' + dep.version; + private static String getId(final Libraries.Dependency dep) { + return dep.group() + ':' + dep.module() + ':' + dep.version(); } } diff --git a/library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java similarity index 57% rename from library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java rename to library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java index b7088272305..73a90aae424 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/InstallerUtils.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java @@ -37,38 +37,47 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; -public final class InstallerUtils { - - // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java +public final class LibraryUtils { private static final char[] hexArray = "0123456789abcdef".toCharArray(); - private InstallerUtils() { + private LibraryUtils() { + } + + public static CompletableFuture asyncFailableFuture(final Callable action, final Executor executor) { + final CompletableFuture future = new CompletableFuture<>(); + executor.execute(() -> { + try { + future.complete(action.call()); + } catch (final Exception ex) { + future.completeExceptionally(ex); + } + }); + return future; } + // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java public static String toHexString(final byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; - hexChars[j * 2] = InstallerUtils.hexArray[v >>> 4]; - hexChars[j * 2 + 1] = InstallerUtils.hexArray[v & 0x0F]; + hexChars[j * 2] = LibraryUtils.hexArray[v >>> 4]; + hexChars[j * 2 + 1] = LibraryUtils.hexArray[v & 0x0F]; } return new String(hexChars); } - public static boolean validateSha1(final String expectedHash, final Path path) throws IOException { - try (final InputStream is = Files.newInputStream(path)) { - return InstallerUtils.validateSha1(expectedHash, is); + public static boolean validateDigest(final String algorithm, final String expectedHash, final Path path) throws IOException, NoSuchAlgorithmException { + try (final InputStream in = Files.newInputStream(path)) { + return LibraryUtils.validateDigest(algorithm, expectedHash, in); } } - public static boolean validateSha1(final String expectedHash, final InputStream stream) throws IOException { - final MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-1"); - } catch (final NoSuchAlgorithmException ex) { - throw new AssertionError(ex); // Guaranteed present by MessageDigest spec - } + public static boolean validateDigest(final String algorithm, final String expectedHash, final InputStream stream) throws IOException, NoSuchAlgorithmException { + final MessageDigest digest = MessageDigest.getInstance(algorithm); final byte[] buf = new byte[4096]; int read; @@ -77,51 +86,27 @@ public static boolean validateSha1(final String expectedHash, final InputStream digest.update(buf,0, read); } - return expectedHash.equals(InstallerUtils.toHexString(digest.digest())); - } - - public static boolean validateSha256(final String expectedHash, final Path path) throws IOException { - try (final InputStream is = Files.newInputStream(path)) { - return InstallerUtils.validateSha256(expectedHash, is); - } - } - - public static boolean validateSha256(final String expectedHash, final InputStream stream) throws IOException { - final MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-256"); - } catch (final NoSuchAlgorithmException ex) { - throw new AssertionError(ex); // Guaranteed present by MessageDigest spec - } - - final byte[] buf = new byte[4096]; - int read; - - while ((read = stream.read(buf)) != -1) { - digest.update(buf,0, read); - } - - return expectedHash.equals(InstallerUtils.toHexString(digest.digest())); + return expectedHash.equals(LibraryUtils.toHexString(digest.digest())); } /** * Downloads a file. * - * @param url The file URL - * @param path The local path + * @param url The source URL + * @param file The destination file * @throws IOException If there is a problem while downloading the file */ - public static void download(final Logger logger, final URL url, final Path path, final boolean requiresRequest) throws IOException { - Files.createDirectories(path.getParent()); + public static void download(final Logger logger, final URL url, final Path file, final boolean requiresRequest) throws IOException { + Files.createDirectories(file.getParent()); - final String name = path.getFileName().toString(); + final String name = file.getFileName().toString(); logger.info("Downloading {}. This may take a while...", name); logger.debug("URL -> <{}>", url); if (!requiresRequest) { - try (final ReadableByteChannel in = Channels.newChannel(url.openStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + try (final ReadableByteChannel in = Channels.newChannel(url.openStream()); + final FileChannel out = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { out.transferFrom(in, 0, Long.MAX_VALUE); } } else { @@ -132,8 +117,8 @@ public static void download(final Logger logger, final URL url, final Path path, connection.connect(); - try (final ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); final FileChannel out = FileChannel.open(path, - StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + try (final ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); + final FileChannel out = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { out.transferFrom(in, 0, Long.MAX_VALUE); } } @@ -142,13 +127,14 @@ public static void download(final Logger logger, final URL url, final Path path, /** * Downloads a file and verify its digest. * - * @param url The file URL - * @param path The local path - * @param expected The SHA-1 expected digest + * @param url The source URL + * @param file The destination file + * @param algorithm The digest algorithm + * @param expected The expected digest * @throws IOException If there is a problem while downloading the file */ - public static void downloadCheckHash(final Logger logger, final URL url, final Path path, final MessageDigest digest, final String expected) throws IOException { - final String name = path.getFileName().toString(); + public static void downloadAndVerifyDigest(final Logger logger, final URL url, final Path file, final String algorithm, final String expected) throws IOException, NoSuchAlgorithmException { + final String name = file.getFileName().toString(); logger.info("Downloading {}. This may take a while...", name); logger.debug("URL -> <{}>", url); @@ -160,37 +146,40 @@ public static void downloadCheckHash(final Logger logger, final URL url, final P connection.connect(); - try (final InputStream is = connection.getInputStream()) { - InstallerUtils.transferCheckHash(logger, is, path, digest, expected); + try (final InputStream in = connection.getInputStream()) { + LibraryUtils.transferAndVerifyDigest(logger, in, file, algorithm, expected); } } /** * Transfer a file and verify its digest. * - * @param source the stream - * @param path The local path - * @param expected The SHA-1 expected digest - * @throws IOException If there is a problem while downloading the file + * @param source The source stream + * @param file The destination file + * @param algorithm The digest algorithm + * @param expected The expected digest + * @throws IOException If there is a problem while transferring the file */ - public static void transferCheckHash(final Logger logger, final InputStream source, final Path path, final MessageDigest digest, final String expected) throws IOException { - Files.createDirectories(path.getParent()); + public static void transferAndVerifyDigest(final Logger logger, final InputStream source, final Path file, final String algorithm, final String expected) throws IOException, NoSuchAlgorithmException { + final MessageDigest digest = MessageDigest.getInstance(algorithm); + + Files.createDirectories(file.getParent()); - final String name = path.getFileName().toString(); + final String name = file.getFileName().toString(); // Pipe the download stream into the file and compute the hash try (final DigestInputStream stream = new DigestInputStream(source, digest); final ReadableByteChannel in = Channels.newChannel(stream); - final FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + final FileChannel out = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { out.transferFrom(in, 0, Long.MAX_VALUE); } - final String fileSha1 = InstallerUtils.toHexString(digest.digest()); + final String fileDigest = LibraryUtils.toHexString(digest.digest()); - if (expected.equalsIgnoreCase(fileSha1)) { + if (expected.equalsIgnoreCase(fileDigest)) { logger.info("Successfully processed {} and verified checksum!", name); } else { - Files.delete(path); - throw new IOException(String.format("Checksum verification for %s failed: Expected '%s', got '%s'.", name, expected, fileSha1)); + Files.delete(file); + throw new IOException(String.format("Checksum verification for %s failed: Expected '%s', got '%s'.", name, expected, fileDigest)); } } } diff --git a/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java index 905bfff8d5e..ba9c90557cc 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java @@ -26,32 +26,7 @@ import java.util.List; import java.util.Map; -import java.util.Objects; -public final class Libraries { - - public Map> dependencies; - - public static final class Dependency { - - public String group, module, version, md5; - - @Override - public int hashCode() { - return Objects.hash(this.group, this.module); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final Dependency that = (Dependency) o; - return this.group.equals(that.group) && - this.module.equals(that.module); - } - } +public record Libraries(Map> dependencies) { + public record Dependency(String group, String module, String version, String md5) {} } diff --git a/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java index 7cd40d7b79c..d9c49d5747a 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java @@ -27,20 +27,7 @@ import java.net.URL; import java.util.List; -public final class SonatypeResponse { - - public List items; - public String continuationToken; - - public static final class Item { - - public URL downloadUrl; - public String path, id, repository, format; - public Checksum checksum; - } - - public static final class Checksum { - - public String sha1, md5; - } +public record SonatypeResponse(List items, String continuationToken) { + public record Item(URL downloadUrl, String path, String id, String repository, String format, Checksum checksum) {} + public record Checksum(String md5, String sha1, String sha256) {} } diff --git a/vanilla/src/installer/java-templates/org/spongepowered/vanilla/installer/Constants.java b/vanilla/src/installer/java-templates/org/spongepowered/vanilla/installer/Constants.java index c7a1027467f..02ebcf1ca3e 100644 --- a/vanilla/src/installer/java-templates/org/spongepowered/vanilla/installer/Constants.java +++ b/vanilla/src/installer/java-templates/org/spongepowered/vanilla/installer/Constants.java @@ -34,9 +34,6 @@ public static final class Libraries { public static final String MINECRAFT_SERVER_JAR_NAME = "minecraft_server"; public static final String MINECRAFT_MAPPINGS_PREFIX = Libraries.MINECRAFT_PATH_PREFIX + "/mappings"; public static final String MINECRAFT_MAPPINGS_NAME = "server.txt"; - - public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?md5=%s&maven" - + ".groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; } public static final class ManifestAttributes { diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java index a6d1f088892..f7f115f98aa 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java @@ -31,9 +31,8 @@ import net.minecraftforge.fart.api.SourceFixerConfig; import net.minecraftforge.fart.api.Transformer; import net.minecraftforge.srgutils.IMappingFile; -import org.spongepowered.libs.AsyncUtils; -import org.spongepowered.libs.InstallerUtils; import org.spongepowered.libs.LibraryManager; +import org.spongepowered.libs.LibraryUtils; import org.spongepowered.vanilla.installer.library.TinyLogger; import org.spongepowered.vanilla.installer.model.GroupArtifactVersion; import org.spongepowered.vanilla.installer.model.mojang.BundleElement; @@ -46,6 +45,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; @@ -58,7 +58,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; @@ -67,10 +66,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -113,7 +112,7 @@ public void downloadAndRun() throws Exception { this.installer.getLibraryManager().validate(); } - final var libraryManager = this.installer.getLibraryManager(); + final LibraryManager libraryManager = this.installer.getLibraryManager(); try { if (mcVersion != null) { final CompletableFuture mappingsFuture = this.downloadMappings(mcVersion, LauncherCommandLine.librariesDirectory); @@ -122,9 +121,9 @@ public void downloadAndRun() throws Exception { .thenApplyAsync(bundle -> this.extractBundle(bundle, LauncherCommandLine.librariesDirectory), libraryManager.preparationWorker()); final CompletableFuture remappedMinecraftJarFuture = mappingsFuture.thenCombineAsync(extractedFuture, (mappings, minecraft) -> { try { - return this.remapMinecraft(minecraft, mappings, this.installer.getLibraryManager().preparationWorker()); + return this.remapMinecraft(minecraft, mappings); } catch (final IOException ex) { - return AsyncUtils.sneakyThrow(ex); + throw new UncheckedIOException(ex); } }, libraryManager.preparationWorker()); libraryManager.validate(); @@ -252,8 +251,7 @@ private Version downloadMinecraftManifest() throws IOException { VersionManifest.Version foundVersionManifest = null; final Gson gson = new Gson(); - final URLConnection conn = new URL(Constants.Libraries.MINECRAFT_MANIFEST_URL) - .openConnection(); + final URLConnection conn = new URL(Constants.Libraries.MINECRAFT_MANIFEST_URL).openConnection(); conn.setConnectTimeout(5 /* seconds */ * 1000); try (final JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream()))) { final VersionManifest manifest = gson.fromJson(reader, VersionManifest.class); @@ -298,29 +296,25 @@ private Path expectedBundleLocation(final Path originalLocation) { } private CompletableFuture downloadMinecraft(final Version version, final Path librariesDirectory) { - return AsyncUtils.asyncFailableFuture(() -> { + return LibraryUtils.asyncFailableFuture(() -> { final Path downloadTarget = this.expectedBundleLocation(this.expectedMinecraftLocation(librariesDirectory, version.id)); if (Files.notExists(downloadTarget)) { if (!this.installer.getLauncherConfig().autoDownloadLibraries) { - throw new IOException( - String.format("The Minecraft jar is not located at '%s' and downloading it has been turned off.", downloadTarget)); + throw new IOException(String.format("The Minecraft jar is not located at '%s' and downloading it has been turned off.", downloadTarget)); } - InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, - MessageDigest.getInstance("SHA-1"), version.downloads.server.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, "SHA-1", version.downloads.server.sha1); } else { if (this.installer.getLauncherConfig().checkLibraryHashes) { Logger.info("Detected existing Minecraft Server jar, verifying hashes..."); // Pipe the download stream into the file and compute the SHA-1 - if (InstallerUtils.validateSha1(version.downloads.server.sha1, downloadTarget)) { + if (LibraryUtils.validateDigest("SHA-1", version.downloads.server.sha1, downloadTarget)) { Logger.info("Minecraft Server jar verified!"); } else { - Logger.error("Checksum verification failed: Expected {}. Deleting cached Minecraft Server jar...", - version.downloads.server.sha1); + Logger.error("Checksum verification failed: Expected {}. Deleting cached Minecraft Server jar...", version.downloads.server.sha1); Files.delete(downloadTarget); - InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, - MessageDigest.getInstance("SHA-1"), version.downloads.server.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, "SHA-1", version.downloads.server.sha1); } } else { Logger.info("Detected existing Minecraft jar. Skipping hash check as that is turned off..."); @@ -333,8 +327,8 @@ private CompletableFuture downloadMinecraft(final Version version, final P private ServerAndLibraries extractBundle(final Path bundleJar, final Path librariesDirectory) { final Path serverDestination = this.expectedMinecraftLocation(librariesDirectory, Constants.Libraries.MINECRAFT_VERSION_TARGET); try (final JarFile bundle = new JarFile(bundleJar.toFile())) { - final var metaOpt = BundlerMetadata.read(bundle); - if (!metaOpt.isPresent()) { + final Optional metaOpt = BundlerMetadata.read(bundle); + if (metaOpt.isEmpty()) { return new ServerAndLibraries(bundleJar, Map.of()); } final BundlerMetadata md = metaOpt.get(); @@ -347,15 +341,15 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar boolean serverExtractionNeeded = true; final BundleElement server = md.server(); if (Files.exists(serverDestination)) { - if (InstallerUtils.validateSha256(server.sha256(), serverDestination)) { + if (LibraryUtils.validateDigest("SHA-256", server.sha256(), serverDestination)) { // library is valid serverExtractionNeeded = false; } } if (serverExtractionNeeded) { final ZipEntry serverEntry = bundle.getEntry(server.path()); - try (final InputStream is = bundle.getInputStream(serverEntry)) { - InstallerUtils.transferCheckHash(TinyLogger.INSTANCE, is, serverDestination, MessageDigest.getInstance("SHA-256"), server.sha256()); + try (final InputStream in = bundle.getInputStream(serverEntry)) { + LibraryUtils.transferAndVerifyDigest(TinyLogger.INSTANCE, in, serverDestination, "SHA-256", server.sha256()); } } @@ -366,7 +360,7 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar final Path destination = gav.resolve(librariesDirectory).resolve(gav.artifact() + '-' + gav.version() + (gav.classifier() == null ? "" : '-' + gav.classifier()) +".jar"); if (Files.exists(destination)) { - if (InstallerUtils.validateSha256(library.sha256(), destination)) { + if (LibraryUtils.validateDigest("SHA-256", library.sha256(), destination)) { // library is valid libs.put(gav, destination); continue; @@ -374,8 +368,8 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar } final ZipEntry entry = bundle.getEntry(library.path()); - try (final InputStream is = bundle.getInputStream(entry)) { - InstallerUtils.transferCheckHash(TinyLogger.INSTANCE, is, destination, MessageDigest.getInstance("SHA-256"), library.sha256()); + try (final InputStream in = bundle.getInputStream(entry)) { + LibraryUtils.transferAndVerifyDigest(TinyLogger.INSTANCE, in, destination, "SHA-256", library.sha256()); libs.put(gav, destination); } } @@ -388,7 +382,7 @@ private ServerAndLibraries extractBundle(final Path bundleJar, final Path librar } private CompletableFuture downloadMappings(final Version version, final Path librariesDirectory) { - return AsyncUtils.asyncFailableFuture(() -> { + return LibraryUtils.asyncFailableFuture(() -> { Logger.info("Setting up names for Minecraft {}", Constants.Libraries.MINECRAFT_VERSION_TARGET); final Path downloadTarget = librariesDirectory.resolve(Constants.Libraries.MINECRAFT_MAPPINGS_PREFIX) .resolve(Constants.Libraries.MINECRAFT_VERSION_TARGET) @@ -403,7 +397,7 @@ private CompletableFuture downloadMappings(final Version version, final Pa if (Files.exists(downloadTarget)) { if (checkHashes) { Logger.info("Detected existing mappings, verifying hashes..."); - if (InstallerUtils.validateSha1(mappings.sha1, downloadTarget)) { + if (LibraryUtils.validateDigest("SHA-1", mappings.sha1, downloadTarget)) { Logger.info("Mappings verified!"); return downloadTarget; } else { @@ -418,10 +412,9 @@ private CompletableFuture downloadMappings(final Version version, final Pa if (this.installer.getLauncherConfig().autoDownloadLibraries) { if (checkHashes) { - InstallerUtils.downloadCheckHash(TinyLogger.INSTANCE, mappings.url, downloadTarget, - MessageDigest.getInstance("SHA-1"), mappings.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, mappings.url, downloadTarget, "SHA-1", mappings.sha1); } else { - InstallerUtils.download(TinyLogger.INSTANCE, mappings.url, downloadTarget, false); + LibraryUtils.download(TinyLogger.INSTANCE, mappings.url, downloadTarget, false); } } else { throw new IOException(String.format("Mappings were not located at '%s' and downloading them has been turned off.", downloadTarget)); @@ -431,7 +424,7 @@ private CompletableFuture downloadMappings(final Version version, final Pa }, this.installer.getLibraryManager().preparationWorker()); } - private ServerAndLibraries remapMinecraft(final ServerAndLibraries minecraft, final Path serverMappings, final ExecutorService service) throws IOException { + private ServerAndLibraries remapMinecraft(final ServerAndLibraries minecraft, final Path serverMappings) throws IOException { Logger.info("Checking if we need to remap Minecraft..."); final Path outputJar = this.expectedRemappedLocation(minecraft.server()); final Path tempOutput = outputJar.resolveSibling(Constants.Libraries.MINECRAFT_SERVER_JAR_NAME + "_remapped.jar.tmp"); From 7ebe3f7541932a390e545a17108a3d50a1b79d7a Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Wed, 20 Nov 2024 22:04:47 +0100 Subject: [PATCH 22/24] Use SHA-512 for libraries verification --- build-logic/build.gradle | 1 + .../gradle/impl/OutputDependenciesToJson.java | 92 +++++-------------- .../spongepowered/libs/LibraryManager.java | 12 +-- .../org/spongepowered/libs/LibraryUtils.java | 17 ++-- .../spongepowered/libs/model/Libraries.java | 2 +- .../libs/model/SonatypeResponse.java | 2 +- 6 files changed, 38 insertions(+), 88 deletions(-) diff --git a/build-logic/build.gradle b/build-logic/build.gradle index 85d251347c1..5cabd817724 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -6,6 +6,7 @@ plugins { indra { javaVersions { strictVersions(false) // it's just buildscript, no need for anything fancy + target(17) } } diff --git a/build-logic/src/main/java/org/spongepowered/gradle/impl/OutputDependenciesToJson.java b/build-logic/src/main/java/org/spongepowered/gradle/impl/OutputDependenciesToJson.java index b34651cc054..c4470d8c0fc 100644 --- a/build-logic/src/main/java/org/spongepowered/gradle/impl/OutputDependenciesToJson.java +++ b/build-logic/src/main/java/org/spongepowered/gradle/impl/OutputDependenciesToJson.java @@ -54,35 +54,20 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; public abstract class OutputDependenciesToJson extends DefaultTask { - // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java - private static final char[] hexArray = "0123456789abcdef".toCharArray(); + private static final char[] hexChars = "0123456789abcdef".toCharArray(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); /** * A single dependency. */ - static final class DependencyDescriptor implements Comparable { - - final String group; - final String module; - final String version; - final String md5; - - DependencyDescriptor(final String group, final String module, final String version, final String md5) { - this.group = group; - this.module = module; - this.version = version; - this.md5 = md5; - } - + record DependencyDescriptor(String group, String module, String version, String sha512) implements Comparable { @Override public int compareTo(final DependencyDescriptor that) { final int group = this.group.compareTo(that.group); @@ -97,35 +82,6 @@ public int compareTo(final DependencyDescriptor that) { return this.version.compareTo(that.version); } - - @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } - if (other == null || this.getClass() != other.getClass()) { - return false; - } - final DependencyDescriptor that = (DependencyDescriptor) other; - return Objects.equals(this.group, that.group) - && Objects.equals(this.module, that.module) - && Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.group, this.module, this.version); - } - - @Override - public String toString() { - return "DependencyDescriptor{" + - "group='" + this.group + '\'' + - ", module='" + this.module + '\'' + - ", version='" + this.version + '\'' + - ", md5='" + this.md5 + '\'' + - '}'; - } } /** @@ -134,14 +90,7 @@ public String toString() { *

At runtime, transitive dependencies won't be traversed, so this needs to * include direct + transitive depends.

*/ - static final class DependencyManifest { - - final Map> dependencies; - - DependencyManifest(final Map> dependencies) { - this.dependencies = dependencies; - } - } + record DependencyManifest(Map> dependencies) {} /** * Configuration to gather dependency artifacts from. @@ -225,27 +174,28 @@ private List configToDescriptor(final Set { final ModuleComponentIdentifier id = (ModuleComponentIdentifier) dependency.getId().getComponentIdentifier(); - // Get file input stream for reading the file content - final String md5hash; + final MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-512"); + } catch (final NoSuchAlgorithmException e) { + throw new GradleException("Failed to find digest algorithm", e); + } + try (final InputStream in = Files.newInputStream(dependency.getFile().toPath())) { - final MessageDigest hasher = MessageDigest.getInstance("MD5"); final byte[] buf = new byte[4096]; int read; while ((read = in.read(buf)) != -1) { - hasher.update(buf, 0, read); + digest.update(buf, 0, read); } - - md5hash = OutputDependenciesToJson.toHexString(hasher.digest()); - } catch (final IOException | NoSuchAlgorithmException ex) { - throw new GradleException("Failed to create hash for " + dependency, ex); + } catch (final IOException e) { + throw new GradleException("Failed to digest file for " + dependency, e); } - // create descriptor return new DependencyDescriptor( id.getGroup(), id.getModule(), id.getVersion(), - md5hash + OutputDependenciesToJson.toHexString(digest.digest()) ); }) .sorted(Comparator.naturalOrder()) // sort dependencies for stable output @@ -253,13 +203,13 @@ private List configToDescriptor(final Set>> 4]; - hexChars[j * 2 + 1] = OutputDependenciesToJson.hexArray[v & 0x0F]; + final char[] chars = new char[bytes.length << 1]; + int i = 0; + for (final byte b : bytes) { + chars[i++] = OutputDependenciesToJson.hexChars[(b >> 4) & 15]; + chars[i++] = OutputDependenciesToJson.hexChars[b & 15]; } - return new String(hexChars); + return new String(chars); } public static class ConfigurationHolder { @@ -274,7 +224,7 @@ public Provider> getIds() { return this.getArtifacts().map(set -> set.stream() .map(art -> art.getId().getComponentIdentifier()) .filter(id -> id instanceof ModuleComponentIdentifier) - .map(art -> art.getDisplayName()) + .map(ComponentIdentifier::getDisplayName) .collect(Collectors.toSet())); } diff --git a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java index eb43baf8c78..8aed4aeff77 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java @@ -53,8 +53,8 @@ import java.util.concurrent.TimeUnit; public final class LibraryManager { - public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?md5=%s&maven" - + ".groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; + public static final String SPONGE_NEXUS_DOWNLOAD_URL = "https://repo.spongepowered.org/service/rest/v1/search/assets?sha512=%s" + + "&maven.groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=jar"; private final Logger logger; private final boolean checkLibraryHashes; @@ -150,17 +150,17 @@ private Set scheduleDownloads( return depFile; } - if (LibraryUtils.validateDigest("MD5", dep.md5(), depFile)) { + if (LibraryUtils.validateDigest("SHA-512", dep.sha512(), depFile)) { this.logger.debug("'{}' verified!", depFile); return depFile; } - this.logger.error("Checksum verification failed: Expected {}. Deleting cached '{}'...", dep.md5(), depFile); + this.logger.error("Checksum verification failed: Expected {}. Deleting cached '{}'...", dep.sha512(), depFile); Files.delete(depFile); } final URL requestUrl = URI.create(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, - dep.md5(), dep.group(), dep.module(), dep.version())).toURL(); + dep.sha512(), dep.group(), dep.module(), dep.version())).toURL(); final SonatypeResponse response = this.getResponseFor(this.gson, requestUrl); if (response.items().isEmpty()) { failures.add("No data received from '" + requestUrl + "'!"); @@ -170,7 +170,7 @@ private Set scheduleDownloads( final SonatypeResponse.Item item = response.items().get(0); if (checkHashes) { - LibraryUtils.downloadAndVerifyDigest(this.logger, item.downloadUrl(), depFile, "MD5", item.checksum().md5()); + LibraryUtils.downloadAndVerifyDigest(this.logger, item.downloadUrl(), depFile, "SHA-512", item.checksum().sha512()); } else { LibraryUtils.download(this.logger, item.downloadUrl(), depFile, true); } diff --git a/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java index 73a90aae424..6d1c2c8d7b6 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryUtils.java @@ -42,7 +42,7 @@ import java.util.concurrent.Executor; public final class LibraryUtils { - private static final char[] hexArray = "0123456789abcdef".toCharArray(); + private static final char[] hexChars = "0123456789abcdef".toCharArray(); private LibraryUtils() { } @@ -59,15 +59,14 @@ public static CompletableFuture asyncFailableFuture(final Callable act return future; } - // From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java public static String toHexString(final byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = LibraryUtils.hexArray[v >>> 4]; - hexChars[j * 2 + 1] = LibraryUtils.hexArray[v & 0x0F]; + final char[] chars = new char[bytes.length << 1]; + int i = 0; + for (final byte b : bytes) { + chars[i++] = LibraryUtils.hexChars[(b >> 4) & 15]; + chars[i++] = LibraryUtils.hexChars[b & 15]; } - return new String(hexChars); + return new String(chars); } public static boolean validateDigest(final String algorithm, final String expectedHash, final Path path) throws IOException, NoSuchAlgorithmException { @@ -175,7 +174,7 @@ public static void transferAndVerifyDigest(final Logger logger, final InputStrea final String fileDigest = LibraryUtils.toHexString(digest.digest()); - if (expected.equalsIgnoreCase(fileDigest)) { + if (expected.equals(fileDigest)) { logger.info("Successfully processed {} and verified checksum!", name); } else { Files.delete(file); diff --git a/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java index ba9c90557cc..2fc97516067 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/Libraries.java @@ -28,5 +28,5 @@ import java.util.Map; public record Libraries(Map> dependencies) { - public record Dependency(String group, String module, String version, String md5) {} + public record Dependency(String group, String module, String version, String sha512) {} } diff --git a/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java index d9c49d5747a..6126f089a6e 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java +++ b/library-manager/src/main/java/org/spongepowered/libs/model/SonatypeResponse.java @@ -29,5 +29,5 @@ public record SonatypeResponse(List items, String continuationToken) { public record Item(URL downloadUrl, String path, String id, String repository, String format, Checksum checksum) {} - public record Checksum(String md5, String sha1, String sha256) {} + public record Checksum(String sha512) {} } From c54c2b5a78283ec54f3570bf0aa40fb9c1478e6d Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Wed, 20 Nov 2024 22:42:28 +0100 Subject: [PATCH 23/24] Cleanup SV installer models --- .../spongepowered/libs/LibraryManager.java | 2 +- .../vanilla/installer/InstallerMain.java | 36 ++++++----- .../installer/model/mojang/Version.java | 58 +++--------------- .../model/mojang/VersionManifest.java | 61 +------------------ .../installer/model/mojang/VersionType.java | 38 ++++++++++++ 5 files changed, 67 insertions(+), 128 deletions(-) create mode 100644 vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionType.java diff --git a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java index 8aed4aeff77..2beebce8ffa 100644 --- a/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java +++ b/library-manager/src/main/java/org/spongepowered/libs/LibraryManager.java @@ -159,7 +159,7 @@ private Set scheduleDownloads( Files.delete(depFile); } - final URL requestUrl = URI.create(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, + final URL requestUrl = new URI(String.format(LibraryManager.SPONGE_NEXUS_DOWNLOAD_URL, dep.sha512(), dep.group(), dep.module(), dep.version())).toURL(); final SonatypeResponse response = this.getResponseFor(this.gson, requestUrl); if (response.items().isEmpty()) { diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java index f7f115f98aa..9c36b84dd5e 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java @@ -245,18 +245,18 @@ private static void bootstrap(final Path[] bootLibs, final Path spongeBoot, fina } } - private Version downloadMinecraftManifest() throws IOException { + private Version downloadMinecraftManifest() throws Exception { Logger.info("Downloading the Minecraft versions manifest..."); VersionManifest.Version foundVersionManifest = null; final Gson gson = new Gson(); - final URLConnection conn = new URL(Constants.Libraries.MINECRAFT_MANIFEST_URL).openConnection(); + final URLConnection conn = new URI(Constants.Libraries.MINECRAFT_MANIFEST_URL).toURL().openConnection(); conn.setConnectTimeout(5 /* seconds */ * 1000); try (final JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream()))) { final VersionManifest manifest = gson.fromJson(reader, VersionManifest.class); - for (final VersionManifest.Version version : manifest.versions) { - if (Constants.Libraries.MINECRAFT_VERSION_TARGET.equals(version.id)) { + for (final VersionManifest.Version version : manifest.versions()) { + if (Constants.Libraries.MINECRAFT_VERSION_TARGET.equals(version.id())) { foundVersionManifest = version; break; } @@ -268,14 +268,12 @@ private Version downloadMinecraftManifest() throws IOException { } final Version version; - - try (final JsonReader reader = new JsonReader(new InputStreamReader(foundVersionManifest.url.openStream()))) { + try (final JsonReader reader = new JsonReader(new InputStreamReader(foundVersionManifest.url().openStream()))) { version = gson.fromJson(reader, Version.class); } if (version == null) { - throw new IOException(String.format("Failed to download version information for '%s'!", - Constants.Libraries.MINECRAFT_VERSION_TARGET)); + throw new IOException(String.format("Failed to download version information for '%s'!", Constants.Libraries.MINECRAFT_VERSION_TARGET)); } return version; @@ -297,24 +295,25 @@ private Path expectedBundleLocation(final Path originalLocation) { private CompletableFuture downloadMinecraft(final Version version, final Path librariesDirectory) { return LibraryUtils.asyncFailableFuture(() -> { - final Path downloadTarget = this.expectedBundleLocation(this.expectedMinecraftLocation(librariesDirectory, version.id)); + final Path downloadTarget = this.expectedBundleLocation(this.expectedMinecraftLocation(librariesDirectory, version.id())); + final Version.Downloads.Download server = version.downloads().server(); if (Files.notExists(downloadTarget)) { if (!this.installer.getLauncherConfig().autoDownloadLibraries) { throw new IOException(String.format("The Minecraft jar is not located at '%s' and downloading it has been turned off.", downloadTarget)); } - LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, "SHA-1", version.downloads.server.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, server.url(), downloadTarget, "SHA-1", server.sha1()); } else { if (this.installer.getLauncherConfig().checkLibraryHashes) { Logger.info("Detected existing Minecraft Server jar, verifying hashes..."); // Pipe the download stream into the file and compute the SHA-1 - if (LibraryUtils.validateDigest("SHA-1", version.downloads.server.sha1, downloadTarget)) { + if (LibraryUtils.validateDigest("SHA-1", server.sha1(), downloadTarget)) { Logger.info("Minecraft Server jar verified!"); } else { - Logger.error("Checksum verification failed: Expected {}. Deleting cached Minecraft Server jar...", version.downloads.server.sha1); + Logger.error("Checksum verification failed: Expected {}. Deleting cached Minecraft Server jar...", server.sha1()); Files.delete(downloadTarget); - LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, version.downloads.server.url, downloadTarget, "SHA-1", version.downloads.server.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, server.url(), downloadTarget, "SHA-1", server.sha1()); } } else { Logger.info("Detected existing Minecraft jar. Skipping hash check as that is turned off..."); @@ -388,7 +387,7 @@ private CompletableFuture downloadMappings(final Version version, final Pa .resolve(Constants.Libraries.MINECRAFT_VERSION_TARGET) .resolve(Constants.Libraries.MINECRAFT_MAPPINGS_NAME); - final Version.Downloads.Download mappings = version.downloads.server_mappings; + final Version.Downloads.Download mappings = version.downloads().server_mappings(); if (mappings == null) { throw new IOException(String.format("Mappings were not included in version manifest for %s", Constants.Libraries.MINECRAFT_VERSION_TARGET)); } @@ -397,12 +396,11 @@ private CompletableFuture downloadMappings(final Version version, final Pa if (Files.exists(downloadTarget)) { if (checkHashes) { Logger.info("Detected existing mappings, verifying hashes..."); - if (LibraryUtils.validateDigest("SHA-1", mappings.sha1, downloadTarget)) { + if (LibraryUtils.validateDigest("SHA-1", mappings.sha1(), downloadTarget)) { Logger.info("Mappings verified!"); return downloadTarget; } else { - Logger.error("Checksum verification failed: Expected {}. Deleting cached server mappings file...", - version.downloads.server.sha1); + Logger.error("Checksum verification failed: Expected {}. Deleting cached server mappings file...", mappings.sha1()); Files.delete(downloadTarget); } } else { @@ -412,9 +410,9 @@ private CompletableFuture downloadMappings(final Version version, final Pa if (this.installer.getLauncherConfig().autoDownloadLibraries) { if (checkHashes) { - LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, mappings.url, downloadTarget, "SHA-1", mappings.sha1); + LibraryUtils.downloadAndVerifyDigest(TinyLogger.INSTANCE, mappings.url(), downloadTarget, "SHA-1", mappings.sha1()); } else { - LibraryUtils.download(TinyLogger.INSTANCE, mappings.url, downloadTarget, false); + LibraryUtils.download(TinyLogger.INSTANCE, mappings.url(), downloadTarget, false); } } else { throw new IOException(String.format("Mappings were not located at '%s' and downloading them has been turned off.", downloadTarget)); diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/Version.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/Version.java index 7bc13638155..78d98fdde5b 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/Version.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/Version.java @@ -25,64 +25,22 @@ package org.spongepowered.vanilla.installer.model.mojang; import java.net.URL; -import java.util.Date; import java.util.List; import java.util.Map; -public final class Version { +public record Version(AssetIndex assetIndex, String assets, Downloads downloads, String id, + List libraries, String mainClass, VersionType type, String releaseTime, String time) { - public AssetIndex assetIndex; - public String assets; - public Downloads downloads; - public String id; - public List libraries; - public String mainClass; - @SuppressWarnings("UseOfObsoleteDateTimeApi") - public Date releaseTime; - @SuppressWarnings("UseOfObsoleteDateTimeApi") - public Date time; - public VersionManifest.Version.Type type; + public record AssetIndex(String id, String sha1, int size, int totalSize, URL url) {} - public static class AssetIndex { - - public String id; - public String sha1; - public int size; - public int totalSize; - public URL url; - } - - public static class Downloads { - - public Download client; - public Download client_mappings; - public Download server; - public Download server_mappings; - - public static class Download { - - public String sha1; - public int size; - public URL url; + public record Downloads(Download client, Download client_mappings, Download server, Download server_mappings) { + public record Download(String sha1, int size, URL url) { } } - public static class Library { - - public org.spongepowered.vanilla.installer.model.mojang.Version.Library.Downloads downloads; - public String name; - - public static class Downloads { - - public Artifact artifact; - public Map classifiers; - - public static class Artifact { - - public String path; - public String sha1; - public int size; - public URL url; + public record Library(Library.Downloads downloads, String name) { + public record Downloads(Artifact artifact, Map classifiers) { + public record Artifact(String path, String sha1, int size, URL url) { } } } diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionManifest.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionManifest.java index fe185e71c45..e2f56d6e2a7 100644 --- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionManifest.java +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionManifest.java @@ -24,65 +24,10 @@ */ package org.spongepowered.vanilla.installer.model.mojang; -import com.google.gson.annotations.SerializedName; - import java.net.URL; -import java.util.Date; import java.util.List; -import java.util.Objects; - -public final class VersionManifest { - - public Latest latest; - public List versions; - - public static class Latest { - - public String snapshot; - public String release; - } - - public static class Version { - - public String id; - public Type type; - public URL url; - @SuppressWarnings("UseOfObsoleteDateTimeApi") - public Date time; - @SuppressWarnings("UseOfObsoleteDateTimeApi") - public Date releaseTime; - - @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } - if (other == null || this.getClass() != other.getClass()) { - return false; - } - final Version that = (Version) other; - return Objects.equals(this.id, that.id) && Objects.equals(this.type, that.type); - } - - @Override - public int hashCode() { - return Objects.hash(this.id, this.type); - } - - @Override - public String toString() { - return this.type + ": " + this.id; - } - public enum Type { - @SerializedName("old_alpha") - OLD_ALPHA, - @SerializedName("old_beta") - OLD_BETA, - @SerializedName("release") - RELEASE, - @SerializedName("snapshot") - SNAPSHOT - } - } +public record VersionManifest(Latest latest, List versions) { + public record Latest(String snapshot, String release) {} + public record Version(String id, VersionType type, URL url, String time, String releaseTime) {} } diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionType.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionType.java new file mode 100644 index 00000000000..30e9aa73118 --- /dev/null +++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/model/mojang/VersionType.java @@ -0,0 +1,38 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.vanilla.installer.model.mojang; + +import com.google.gson.annotations.SerializedName; + +public enum VersionType { + @SerializedName("old_alpha") + OLD_ALPHA, + @SerializedName("old_beta") + OLD_BETA, + @SerializedName("release") + RELEASE, + @SerializedName("snapshot") + SNAPSHOT +} From 1fff0942ef29a492f2baf0b148462dfbcda048a1 Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Thu, 21 Nov 2024 21:27:05 +0100 Subject: [PATCH 24/24] Disable spotless on forge until ready --- .github/workflows/check-spotless.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-spotless.yaml b/.github/workflows/check-spotless.yaml index 5919a3117ef..acbdc2e8064 100644 --- a/.github/workflows/check-spotless.yaml +++ b/.github/workflows/check-spotless.yaml @@ -11,5 +11,5 @@ jobs: uses: SpongePowered/.github/.github/workflows/shared-check-spotless.yaml@master with: runtime_version: 21 - extra_gradle_params: "-Pprojects=vanilla,forge,neoforge,testplugins" + extra_gradle_params: "-Pprojects=vanilla,neoforge,testplugins" secrets: inherit