From 903e61f1a38dd66d0bd314d8ca0d6af4cc0657db Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 17 Jun 2023 03:39:53 +0200 Subject: [PATCH] Exposing resourcepack loading to api (#3696) Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> Co-authored-by: RednedEpic --- .../org/geysermc/geyser/api/GeyserApi.java | 17 ++ .../SessionLoadResourcePacksEvent.java | 67 ++++++ .../geysermc/geyser/api/pack/PackCodec.java | 82 +++++++ .../geyser/api/pack/PathPackCodec.java | 45 ++++ .../geyser/api/pack/ResourcePack.java | 72 ++++++ .../geyser/api/pack/ResourcePackManifest.java | 209 ++++++++++++++++++ core/build.gradle.kts | 2 +- .../java/org/geysermc/geyser/GeyserImpl.java | 19 +- .../SessionLoadResourcePacksEventImpl.java | 69 ++++++ .../geyser/network/UpstreamPacketHandler.java | 71 +++--- .../geyser/pack/GeyserResourcePack.java | 38 ++++ .../pack/GeyserResourcePackManifest.java | 65 ++++++ .../geysermc/geyser/pack/ResourcePack.java | 160 -------------- .../geyser/pack/ResourcePackManifest.java | 117 ---------- .../geyser/pack/path/GeyserPathPackCodec.java | 109 +++++++++ .../geyser/registry/DeferredRegistry.java | 180 +++++++++++++++ .../geysermc/geyser/registry/IRegistry.java | 60 +++++ .../geysermc/geyser/registry/Registries.java | 7 + .../geysermc/geyser/registry/Registry.java | 5 +- .../loader/ProviderRegistryLoader.java | 9 +- .../registry/loader/RegistryLoaders.java | 13 +- .../registry/loader/ResourcePackLoader.java | 156 +++++++++++++ .../geysermc/geyser/skin/ProvidedSkins.java | 9 +- .../geysermc/geyser/text/MinecraftLocale.java | 13 +- .../org/geysermc/geyser/util/AssetUtils.java | 4 +- .../org/geysermc/geyser/util/FileUtils.java | 26 +-- .../loader/ResourcePackLoaderTest.java | 57 +++++ 27 files changed, 1334 insertions(+), 347 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java create mode 100644 core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java delete mode 100644 core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java delete mode 100644 core/src/main/java/org/geysermc/geyser/pack/ResourcePackManifest.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/DeferredRegistry.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/IRegistry.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java create mode 100644 core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java diff --git a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java index f86206d36..84cb445ad 100644 --- a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java +++ b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java @@ -36,6 +36,7 @@ import org.geysermc.geyser.api.extension.ExtensionManager; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; +import java.nio.file.Path; import java.util.List; import java.util.UUID; @@ -107,6 +108,22 @@ public interface GeyserApi extends GeyserApiBase { @NonNull BedrockListener bedrockListener(); + /** + * Gets the {@link Path} to the Geyser config directory. + * + * @return the path to the Geyser config directory + */ + @NonNull + Path configDirectory(); + + /** + * Gets the {@link Path} to the Geyser packs directory. + * + * @return the path to the Geyser packs directory + */ + @NonNull + Path packDirectory(); + /** * Gets the current {@link GeyserApiBase} instance. * diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java new file mode 100644 index 000000000..c2f1cd427 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.bedrock; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; +import org.geysermc.geyser.api.pack.ResourcePack; + +import java.util.List; +import java.util.UUID; + +/** + * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending resource packs. + */ +public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { + public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { + super(connection); + } + + /** + * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. + * + * @return an unmodifiable list of resource packs that will be sent to the client. + */ + public abstract @NonNull List resourcePacks(); + + /** + * Registers a {@link ResourcePack} to be sent to the client. + * + * @param resourcePack a resource pack that will be sent to the client. + * @return true if the resource pack was added successfully, + * or false if already present + */ + public abstract boolean register(@NonNull ResourcePack resourcePack); + + /** + * Unregisters a resource pack from being sent to the client. + * + * @param uuid the UUID of the resource pack + * @return true whether the resource pack was removed from the list of resource packs. + */ + public abstract boolean unregister(@NonNull UUID uuid); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java new file mode 100644 index 000000000..884129fa3 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Path; + +/** + * Represents a pack codec that can be used + * to provide resource packs to clients. + */ +public abstract class PackCodec { + + /** + * Gets the sha256 hash of the resource pack. + * + * @return the hash of the resource pack + */ + public abstract byte @NonNull [] sha256(); + + /** + * Gets the resource pack size. + * + * @return the resource pack file size + */ + public abstract long size(); + + /** + * Serializes the given resource pack into a byte buffer. + * + * @param resourcePack the resource pack to serialize + * @return the serialized resource pack + */ + @NonNull + public abstract SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException; + + /** + * Creates a new resource pack from this codec. + * + * @return the new resource pack + */ + @NonNull + protected abstract ResourcePack create(); + + /** + * Creates a new pack provider from the given path. + * + * @param path the path to create the pack provider from + * @return the new pack provider + */ + @NonNull + public static PackCodec path(@NonNull Path path) { + return GeyserApi.api().provider(PathPackCodec.class, path); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java new file mode 100644 index 000000000..ee5db8242 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.nio.file.Path; + +/** + * Represents a pack codec that creates a resource + * pack from a path on the filesystem. + */ +public abstract class PathPackCodec extends PackCodec { + + /** + * Gets the path of the resource pack. + * + * @return the path of the resource pack + */ + @NonNull + public abstract Path path(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java new file mode 100644 index 000000000..de1beaf65 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a resource pack sent to Bedrock clients + *

+ * This representation of a resource pack only contains what + * Geyser requires to send it to the client. + */ +public interface ResourcePack { + + /** + * The {@link PackCodec codec} for this pack. + * + * @return the codec for this pack + */ + @NonNull + PackCodec codec(); + + /** + * Gets the resource pack manifest. + * + * @return the resource pack manifest + */ + @NonNull + ResourcePackManifest manifest(); + + /** + * Gets the content key of the resource pack. Lack of a content key is represented by an empty String. + * + * @return the content key of the resource pack + */ + @NonNull + String contentKey(); + + /** + * Creates a resource pack with the given {@link PackCodec}. + * + * @param codec the pack codec + * @return the resource pack + */ + @NonNull + static ResourcePack create(@NonNull PackCodec codec) { + return codec.create(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java new file mode 100644 index 000000000..c9ccdd6c5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * Represents a resource pack manifest. + */ +public interface ResourcePackManifest { + + /** + * Gets the format version of the resource pack. + * + * @return the format version + */ + int formatVersion(); + + /** + * Gets the header of the resource pack. + * + * @return the header + */ + @NonNull + Header header(); + + /** + * Gets the modules of the resource pack. + * + * @return the modules + */ + @NonNull + Collection modules(); + + /** + * Gets the dependencies of the resource pack. + * + * @return the dependencies + */ + @NonNull + Collection dependencies(); + + /** + * Represents the header of a resource pack. + */ + interface Header { + + /** + * Gets the UUID of the resource pack. + * + * @return the UUID + */ + @NonNull + UUID uuid(); + + /** + * Gets the version of the resource pack. + * + * @return the version + */ + @NonNull + Version version(); + + /** + * Gets the name of the resource pack. + * + * @return the name + */ + @NonNull + String name(); + + /** + * Gets the description of the resource pack. + * + * @return the description + */ + @NonNull + String description(); + + /** + * Gets the minimum supported Minecraft version of the resource pack. + * + * @return the minimum supported Minecraft version + */ + @NonNull + Version minimumSupportedMinecraftVersion(); + } + + /** + * Represents a module of a resource pack. + */ + interface Module { + + /** + * Gets the UUID of the module. + * + * @return the UUID + */ + @NonNull + UUID uuid(); + + /** + * Gets the version of the module. + * + * @return the version + */ + @NonNull + Version version(); + + /** + * Gets the type of the module. + * + * @return the type + */ + @NonNull + String type(); + + /** + * Gets the description of the module. + * + * @return the description + */ + @NonNull + String description(); + } + + /** + * Represents a dependency of a resource pack. + */ + interface Dependency { + + /** + * Gets the UUID of the dependency. + * + * @return the uuid + */ + @NonNull + UUID uuid(); + + /** + * Gets the version of the dependency. + * + * @return the version + */ + @NonNull + Version version(); + } + + /** + * Represents a version of a resource pack. + */ + interface Version { + + /** + * Gets the major version. + * + * @return the major version + */ + int major(); + + /** + * Gets the minor version. + * + * @return the minor version + */ + int minor(); + + /** + * Gets the patch version. + * + * @return the patch version + */ + int patch(); + + /** + * Gets the version formatted as a String. + * + * @return the version string + */ + @NonNull String toString(); + } +} + diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 97540f96e..4d94479c3 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { } implementation(libs.raknet) { - exclude("io.netty", "*"); + exclude("io.netty", "*") } implementation(libs.netty.resolver.dns) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 8204cfd3b..796e5ed06 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -69,9 +69,9 @@ import org.geysermc.geyser.event.GeyserEventBus; import org.geysermc.geyser.extension.GeyserExtensionManager; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.netty.GeyserServer; -import org.geysermc.geyser.pack.ResourcePack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.RegistryLoaders; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; @@ -90,6 +90,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.file.Path; import java.security.Key; import java.text.DecimalFormat; import java.util.*; @@ -258,7 +259,7 @@ public class GeyserImpl implements GeyserApi { SkinProvider.registerCacheImageTask(this); - ResourcePack.loadPacks(); + Registries.RESOURCE_PACKS.load(); String geyserUdpPort = System.getProperty("geyserUdpPort", ""); String pluginUdpPort = geyserUdpPort.isEmpty() ? System.getProperty("pluginUdpPort", "") : geyserUdpPort; @@ -622,7 +623,7 @@ public class GeyserImpl implements GeyserApi { this.erosionUnixListener.close(); } - ResourcePack.PACKS.clear(); + Registries.RESOURCE_PACKS.get().clear(); this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus)); this.extensionManager.disableExtensions(); @@ -681,6 +682,18 @@ public class GeyserImpl implements GeyserApi { return getConfig().getBedrock(); } + @Override + @NonNull + public Path configDirectory() { + return bootstrap.getConfigFolder(); + } + + @Override + @NonNull + public Path packDirectory() { + return bootstrap.getConfigFolder().resolve("packs"); + } + public int buildNumber() { if (!this.isProductionEnvironment()) { return 0; diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java new file mode 100644 index 000000000..5ed0f8d22 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.event.type; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { + + private final Map packs; + + public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { + super(session); + this.packs = packMap; + } + + public @NonNull Map getPacks() { + return packs; + } + + @Override + public @NonNull List resourcePacks() { + return List.copyOf(packs.values()); + } + + @Override + public boolean register(@NonNull ResourcePack resourcePack) { + String packID = resourcePack.manifest().header().uuid().toString(); + if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { + return false; + } + packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); + return true; + } + + @Override + public boolean unregister(@NonNull UUID uuid) { + return packs.remove(uuid.toString()) != null; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 225f8207a..b7955e218 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -49,9 +49,12 @@ import org.cloudburstmc.protocol.common.PacketSignal; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.network.AuthType; +import org.geysermc.geyser.api.pack.PackCodec; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.pack.ResourcePack; -import org.geysermc.geyser.pack.ResourcePackManifest; +import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; +import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; @@ -61,16 +64,20 @@ import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; import org.geysermc.geyser.util.VersionCheckUtils; -import java.io.FileInputStream; -import java.io.InputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; import java.util.ArrayDeque; import java.util.Deque; +import java.util.HashMap; import java.util.OptionalInt; public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; - private Deque packsToSent = new ArrayDeque<>(); + private final Deque packsToSent = new ArrayDeque<>(); + + private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) { super(geyser, session); @@ -172,12 +179,16 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { geyser.getSessionManager().addPendingSession(session); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get())); + this.geyser.eventBus().fire(this.resourcePackLoadEvent); + ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); - for(ResourcePack resourcePack : ResourcePack.PACKS.values()) { - ResourcePackManifest.Header header = resourcePack.getManifest().getHeader(); + for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { + PackCodec codec = pack.codec(); + ResourcePackManifest.Header header = pack.manifest().header(); resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( - header.getUuid().toString(), header.getVersionString(), resourcePack.getFile().length(), - resourcePack.getContentKey(), "", header.getUuid().toString(), false, false)); + header.uuid().toString(), header.version().toString(), codec.size(), pack.contentKey(), + "", header.uuid().toString(), false, false)); } resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); @@ -210,9 +221,9 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not stackPacket.setGameVersion(session.getClientData().getGameVersion()); - for (ResourcePack pack : ResourcePack.PACKS.values()) { - ResourcePackManifest.Header header = pack.getManifest().getHeader(); - stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.getUuid().toString(), header.getVersionString(), "")); + for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { + ResourcePackManifest.Header header = pack.manifest().header(); + stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.uuid().toString(), header.version().toString(), "")); } if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { @@ -291,21 +302,22 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public PacketSignal handle(ResourcePackChunkRequestPacket packet) { ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); - ResourcePack pack = ResourcePack.PACKS.get(packet.getPackId().toString()); + ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId().toString()); + PackCodec codec = pack.codec(); data.setChunkIndex(packet.getChunkIndex()); - data.setProgress(packet.getChunkIndex() * ResourcePack.CHUNK_SIZE); + data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE); data.setPackVersion(packet.getPackVersion()); data.setPackId(packet.getPackId()); - int offset = packet.getChunkIndex() * ResourcePack.CHUNK_SIZE; - long remainingSize = pack.getFile().length() - offset; - byte[] packData = new byte[(int) MathUtils.constrain(remainingSize, 0, ResourcePack.CHUNK_SIZE)]; + int offset = packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE; + long remainingSize = codec.size() - offset; + byte[] packData = new byte[(int) MathUtils.constrain(remainingSize, 0, GeyserResourcePack.CHUNK_SIZE)]; - try (InputStream inputStream = new FileInputStream(pack.getFile())) { - inputStream.skip(offset); - inputStream.read(packData, 0, packData.length); - } catch (Exception e) { + try (SeekableByteChannel channel = codec.serialize(pack)) { + channel.position(offset); + channel.read(ByteBuffer.wrap(packData, 0, packData.length)); + } catch (IOException e) { e.printStackTrace(); } @@ -314,7 +326,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.sendUpstreamPacket(data); // Check if it is the last chunk and send next pack in queue when available. - if (remainingSize <= ResourcePack.CHUNK_SIZE && !packsToSent.isEmpty()) { + if (remainingSize <= GeyserResourcePack.CHUNK_SIZE && !packsToSent.isEmpty()) { sendPackDataInfo(packsToSent.pop()); } @@ -324,15 +336,16 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private void sendPackDataInfo(String id) { ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket(); String[] packID = id.split("_"); - ResourcePack pack = ResourcePack.PACKS.get(packID[0]); - ResourcePackManifest.Header header = pack.getManifest().getHeader(); + ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packID[0]); + PackCodec codec = pack.codec(); + ResourcePackManifest.Header header = pack.manifest().header(); - data.setPackId(header.getUuid()); - int chunkCount = (int) Math.ceil((int) pack.getFile().length() / (double) ResourcePack.CHUNK_SIZE); + data.setPackId(header.uuid()); + int chunkCount = (int) Math.ceil(codec.size() / (double) GeyserResourcePack.CHUNK_SIZE); data.setChunkCount(chunkCount); - data.setCompressedPackSize(pack.getFile().length()); - data.setMaxChunkSize(ResourcePack.CHUNK_SIZE); - data.setHash(pack.getSha256()); + data.setCompressedPackSize(codec.size()); + data.setMaxChunkSize(GeyserResourcePack.CHUNK_SIZE); + data.setHash(codec.sha256()); data.setPackVersion(packID[1]); data.setPremium(false); data.setType(ResourcePackType.RESOURCES); diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java new file mode 100644 index 000000000..82408b6e7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack; + +import org.geysermc.geyser.api.pack.PackCodec; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; + +public record GeyserResourcePack(PackCodec codec, ResourcePackManifest manifest, String contentKey) implements ResourcePack { + + /** + * The size of each chunk to use when sending the resource packs to clients in bytes + */ + public static final int CHUNK_SIZE = 102400; +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java new file mode 100644 index 000000000..ddcee6ad5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Collection; +import java.util.UUID; + +public record GeyserResourcePackManifest(@JsonProperty("format_version") int formatVersion, Header header, Collection modules, Collection dependencies) implements ResourcePackManifest { + + public record Header(UUID uuid, Version version, String name, String description, @JsonProperty("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { } + + public record Module(UUID uuid, Version version, String type, String description) implements ResourcePackManifest.Module { } + + public record Dependency(UUID uuid, Version version) implements ResourcePackManifest.Dependency { } + + @JsonDeserialize(using = Version.VersionDeserializer.class) + public record Version(int major, int minor, int patch) implements ResourcePackManifest.Version { + + @Override + public @NotNull String toString() { + return major + "." + minor + "." + patch; + } + + public static class VersionDeserializer extends JsonDeserializer { + @Override + public Version deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + int[] version = ctxt.readValue(p, int[].class); + return new Version(version[0], version[1], version[2]); + } + } + } +} + diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java deleted file mode 100644 index 07d56194a..000000000 --- a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * 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. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.pack; - -import lombok.Getter; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; -import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** - * This represents a resource pack and all the data relevant to it - */ -public class ResourcePack { - /** - * The list of loaded resource packs - */ - public static final Map PACKS = new HashMap<>(); - - /** - * The size of each chunk to use when sending the resource packs to clients in bytes - */ - public static final int CHUNK_SIZE = 102400; - - private byte[] sha256; - private File file; - private ResourcePackManifest manifest; - private ResourcePackManifest.Version version; - - @Getter - private String contentKey; - - /** - * Loop through the packs directory and locate valid resource pack files - */ - public static void loadPacks() { - Path directory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("packs"); - - if (!Files.exists(directory)) { - try { - Files.createDirectory(directory); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not create packs directory", e); - } - - // As we just created the directory it will be empty - return; - } - - List resourcePacks; - try { - resourcePacks = Files.walk(directory).collect(Collectors.toList()); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not list packs directory", e); - return; - } - - GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); - GeyserImpl.getInstance().eventBus().fire(event); - - for (Path path : event.resourcePacks()) { - File file = path.toFile(); - - if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) { - ResourcePack pack = new ResourcePack(); - - pack.sha256 = FileUtils.calculateSHA256(file); - - try (ZipFile zip = new ZipFile(file); - Stream stream = zip.stream()) { - stream.forEach((x) -> { - String name = x.getName(); - if (name.length() >= 80) { - GeyserImpl.getInstance().getLogger().warning("The resource pack " + file.getName() - + " has a file in it that meets or exceeds 80 characters in its path (" + name - + ", " + name.length() + " characters long). This will cause problems on some Bedrock platforms." + - " Please rename it to be shorter, or reduce the amount of folders needed to get to the file."); - } - if (name.contains("manifest.json")) { - try { - ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); - // Sometimes a pack_manifest file is present and not in a valid format, - // but a manifest file is, so we null check through that one - if (manifest.getHeader().getUuid() != null) { - pack.file = file; - pack.manifest = manifest; - pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); - - PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - - // Check if a file exists with the same name as the resource pack suffixed by .key, - // and set this as content key. (e.g. test.zip, key file would be test.zip.key) - File keyFile = new File(file.getParentFile(), file.getName() + ".key"); - pack.contentKey = keyFile.exists() ? Files.readString(keyFile.toPath(), StandardCharsets.UTF_8) : ""; - } catch (Exception e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", file.getName())); - e.printStackTrace(); - } - } - } - } - - public byte[] getSha256() { - return sha256; - } - - public File getFile() { - return file; - } - - public ResourcePackManifest getManifest() { - return manifest; - } - - public ResourcePackManifest.Version getVersion() { - return version; - } -} diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePackManifest.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePackManifest.java deleted file mode 100644 index 2b14eade3..000000000 --- a/core/src/main/java/org/geysermc/geyser/pack/ResourcePackManifest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * 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. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.pack; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.Value; - -import java.util.Collection; -import java.util.Collections; -import java.util.UUID; - -/** - * author: NukkitX - * Nukkit Project - */ -@Getter -@EqualsAndHashCode -public class ResourcePackManifest { - @JsonProperty("format_version") - private Integer formatVersion; - private Header header; - private Collection modules; - protected Collection dependencies; - - public Collection getModules() { - return Collections.unmodifiableCollection(modules); - } - - @Getter - @ToString - public static class Header { - private String description; - private String name; - private UUID uuid; - private int[] version; - @JsonProperty("min_engine_version") - private int[] minimumSupportedMinecraftVersion; - - public String getVersionString() { - return version[0] + "." + version[1] + "." + version[2]; - } - } - - @Getter - @ToString - public static class Module { - private String description; - private String name; - private UUID uuid; - private int[] version; - } - - @Getter - @ToString - public static class Dependency { - private UUID uuid; - private int[] version; - } - - @Value - public static class Version { - private final int major; - private final int minor; - private final int patch; - - public static Version fromString(String ver) { - String[] split = ver.replace(']', ' ') - .replace('[', ' ') - .replaceAll(" ", "").split(","); - - return new Version(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); - } - - public static Version fromArray(int[] ver) { - return new Version(ver[0], ver[1], ver[2]); - } - - private Version(int major, int minor, int patch) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - - @Override - public String toString() { - return major + "." + minor + "." + patch; - } - } -} - diff --git a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java new file mode 100644 index 000000000..84067600f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack.path; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; +import org.geysermc.geyser.util.FileUtils; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +@RequiredArgsConstructor +public class GeyserPathPackCodec extends PathPackCodec { + private final Path path; + private FileTime lastModified; + + private byte[] sha256; + private long size = -1; + + @Override + public @NonNull Path path() { + this.checkLastModified(); + return this.path; + } + + @Override + public byte @NonNull [] sha256() { + this.checkLastModified(); + if (this.sha256 != null) { + return this.sha256; + } + + return this.sha256 = FileUtils.calculateSHA256(this.path); + } + + @Override + public long size() { + this.checkLastModified(); + if (this.size != -1) { + return this.size; + } + + try { + return this.size = Files.size(this.path); + } catch (IOException e) { + throw new RuntimeException("Could not get file size of path " + this.path, e); + } + } + + @Override + public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + return FileChannel.open(this.path); + } + + @Override + protected @NonNull ResourcePack create() { + return ResourcePackLoader.readPack(this.path); + } + + private void checkLastModified() { + try { + FileTime lastModified = Files.getLastModifiedTime(this.path); + if (this.lastModified == null) { + this.lastModified = lastModified; + return; + } + + if (lastModified.toInstant().isAfter(this.lastModified.toInstant())) { + GeyserImpl.getInstance().getLogger().warning("Detected a change in the resource pack " + path + ". This is likely to cause undefined behavior for new clients joining. It is suggested you restart Geyser."); + this.lastModified = lastModified; + this.sha256 = null; + this.size = -1; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/DeferredRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/DeferredRegistry.java new file mode 100644 index 000000000..bf3050a61 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/DeferredRegistry.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry; + +import org.geysermc.geyser.registry.loader.RegistryLoader; +import org.geysermc.geyser.registry.loader.RegistryLoaders; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A deferred registry is a registry that is not loaded until it is needed. + * This is useful for registries that are not needed until after other parts + * of the lifecycle have been completed. + *

+ * This class is slightly different from other registries in that it acts as + * a wrapper around another registry. This is to allow for any kind of registry + * type to be deferred. + * + * @param the value being held by the registry + */ +public final class DeferredRegistry implements IRegistry { + private final Registry backingRegistry; + private final Supplier loader; + + private boolean loaded; + + private DeferredRegistry(Function, Registry> registryLoader, RegistryLoader deferredLoader) { + this.backingRegistry = registryLoader.apply(RegistryLoaders.uninitialized()); + this.loader = () -> deferredLoader.load(null); + } + + private DeferredRegistry(Function, Registry> registryLoader, Supplier> deferredLoader) { + this.backingRegistry = registryLoader.apply(RegistryLoaders.uninitialized()); + this.loader = () -> deferredLoader.get().load(null); + } + + private DeferredRegistry(I input, RegistryInitializer registryInitializer, RegistryLoader deferredLoader) { + this.backingRegistry = registryInitializer.initialize(input, RegistryLoaders.uninitialized()); + this.loader = () -> deferredLoader.load(input); + } + + private DeferredRegistry(I input, RegistryInitializer registryInitializer, Supplier> deferredLoader) { + this.backingRegistry = registryInitializer.initialize(input, RegistryLoaders.uninitialized()); + this.loader = () -> deferredLoader.get().load(input); + } + + @Override + public M get() { + if (!this.loaded) { + throw new IllegalStateException("Registry has not been loaded yet!"); + } + + return this.backingRegistry.get(); + } + + @Override + public void set(M mappings) { + if (!this.loaded) { + throw new IllegalStateException("Registry has not been loaded yet!"); + } + + this.backingRegistry.set(mappings); + } + + @Override + public void register(Consumer consumer) { + if (!this.loaded) { + throw new IllegalStateException("Registry has not been loaded yet!"); + } + + this.backingRegistry.register(consumer); + } + + /** + * Loads the registry. + */ + public void load() { + if (this.loaded) { + throw new IllegalStateException("Registry has already been loaded!"); + } + + this.backingRegistry.set(this.loader.get()); + this.loaded = true; + } + + /** + * Creates a new deferred registry. + * + * @param registryLoader the registry loader + * @param deferredLoader the deferred loader + * @param the input type + * @param the registry type + * @return the new deferred registry + */ + public static DeferredRegistry create(Function, Registry> registryLoader, RegistryLoader deferredLoader) { + return new DeferredRegistry<>(registryLoader, deferredLoader); + } + + /** + * Creates a new deferred registry. + * + * @param registryLoader the registry loader + * @param deferredLoader the deferred loader + * @param the input type + * @param the registry type + * @return the new deferred registry + */ + public static DeferredRegistry create(Function, Registry> registryLoader, Supplier> deferredLoader) { + return new DeferredRegistry<>(registryLoader, deferredLoader); + } + + /** + * Creates a new deferred registry. + * + * @param registryInitializer the registry initializer + * @param deferredLoader the deferred loader + * @param the input type + * @param the registry type + * @return the new deferred registry + */ + public static DeferredRegistry create(I input, RegistryInitializer registryInitializer, RegistryLoader deferredLoader) { + return new DeferredRegistry<>(input, registryInitializer, deferredLoader); + } + + /** + * Creates a new deferred registry. + * + * @param registryInitializer the registry initializer + * @param deferredLoader the deferred loader + * @param the input type + * @param the registry type + * @return the new deferred registry + */ + public static DeferredRegistry create(I input, RegistryInitializer registryInitializer, Supplier> deferredLoader) { + return new DeferredRegistry<>(input, registryInitializer, deferredLoader); + } + + /** + * A registry initializer. + * + * @param the registry type + */ + interface RegistryInitializer { + + /** + * Initializes the registry. + * + * @param input the input + * @param registryLoader the registry loader + * @param the input type + * @return the initialized registry + */ + Registry initialize(I input, RegistryLoader registryLoader); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/IRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/IRegistry.java new file mode 100644 index 000000000..fdb5f8c77 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/IRegistry.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry; + +import java.util.function.Consumer; + +/** + * Represents a registry. + * + * @param the value being held by the registry + */ +interface IRegistry { + + /** + * Gets the underlying value held by this registry. + * + * @return the underlying value held by this registry. + */ + M get(); + + /** + * Sets the underlying value held by this registry. + * Clears any existing data associated with the previous + * value. + * + * @param mappings the underlying value held by this registry + */ + void set(M mappings); + + /** + * Registers what is specified in the given + * {@link Consumer} into the underlying value. + * + * @param consumer the consumer + */ + void register(Consumer consumer); +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 0bf6ae078..73ae23e9e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -41,6 +41,8 @@ import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; @@ -158,6 +160,11 @@ public final class Registries { */ public static final IntMappedRegistry RECORDS = IntMappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + /** + * A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys. + */ + public static final DeferredRegistry> RESOURCE_PACKS = DeferredRegistry.create(GeyserImpl.getInstance().packDirectory(), SimpleMappedRegistry::create, RegistryLoaders.RESOURCE_PACKS); + /** * A mapped registry holding sound identifiers to their corresponding {@link SoundMapping}. */ diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registry.java b/core/src/main/java/org/geysermc/geyser/registry/Registry.java index afee87b08..f6be19af2 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registry.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registry.java @@ -64,7 +64,7 @@ import java.util.function.Consumer; * * @param the value being held by the registry */ -public abstract class Registry { +public abstract class Registry implements IRegistry { protected M mappings; /** @@ -85,6 +85,7 @@ public abstract class Registry { * * @return the underlying value held by this registry. */ + @Override public M get() { return this.mappings; } @@ -96,6 +97,7 @@ public abstract class Registry { * * @param mappings the underlying value held by this registry */ + @Override public void set(M mappings) { this.mappings = mappings; } @@ -106,6 +108,7 @@ public abstract class Registry { * * @param consumer the consumer */ + @Override public void register(Consumer consumer) { consumer.accept(this.mappings); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 99a9213fe..d32b11cc0 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -31,13 +31,16 @@ import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; +import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; +import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; +import java.nio.file.Path; import java.util.Map; /** @@ -47,11 +50,15 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov @Override public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { + // misc providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Extension) args[0])); + providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); + providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); + + // items providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder()); providers.put(CustomItemOptions.Builder.class, args -> new GeyserCustomItemOptions.CustomItemOptionsBuilder()); providers.put(NonVanillaCustomItemData.Builder.class, args -> new GeyserNonVanillaCustomItemData.NonVanillaCustomItemDataBuilder()); - providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); return providers; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/RegistryLoaders.java b/core/src/main/java/org/geysermc/geyser/registry/loader/RegistryLoaders.java index 62b6e4737..eaede3b15 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/RegistryLoaders.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/RegistryLoaders.java @@ -36,7 +36,12 @@ public final class RegistryLoaders { /** * The {@link RegistryLoader} responsible for loading NBT. */ - public static NbtRegistryLoader NBT = new NbtRegistryLoader(); + public static final NbtRegistryLoader NBT = new NbtRegistryLoader(); + + /** + * The {@link RegistryLoader} responsible for loading resource packs. + */ + public static final ResourcePackLoader RESOURCE_PACKS = new ResourcePackLoader(); /** * Wraps the surrounding {@link Supplier} in a {@link RegistryLoader} which does @@ -51,10 +56,14 @@ public final class RegistryLoaders { } /** + * Returns a {@link RegistryLoader} which has not taken + * in any input value. + * + * @param the input * @param the value * @return a RegistryLoader that is yet to contain a value. */ - public static RegistryLoader uninitialized() { + public static RegistryLoader uninitialized() { return input -> null; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java new file mode 100644 index 000000000..afb5a2fe8 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.loader; + +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.GeyserResourcePackManifest; +import org.geysermc.geyser.pack.path.GeyserPathPackCodec; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.FileUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserLoadResourcePacksEvent}. + */ +public class ResourcePackLoader implements RegistryLoader> { + + static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); + + private static final boolean SHOW_RESOURCE_PACK_LENGTH_WARNING = Boolean.parseBoolean(System.getProperty("Geyser.ShowResourcePackLengthWarning", "true")); + + /** + * Loop through the packs directory and locate valid resource pack files + */ + @Override + public Map load(Path directory) { + Map packMap = new HashMap<>(); + + if (!Files.exists(directory)) { + try { + Files.createDirectory(directory); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not create packs directory", e); + } + } + + List resourcePacks; + try (Stream stream = Files.walk(directory)) { + resourcePacks = stream.filter(PACK_MATCHER::matches) + .collect(Collectors.toCollection(ArrayList::new)); // toList() does not guarantee mutability + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Could not list packs directory", e); + + // Ensure the event is fired even if there was an issue reading + // from our own resource pack directory. External projects may have + // resource packs located at different locations. + resourcePacks = new ArrayList<>(); + } + + GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); + GeyserImpl.getInstance().eventBus().fire(event); + + for (Path path : event.resourcePacks()) { + try { + GeyserResourcePack pack = readPack(path); + packMap.put(pack.manifest().header().uuid().toString(), pack); + } catch (Exception e) { + e.printStackTrace(); + } + } + return packMap; + } + + /** + * Reads a resource pack at the given file. Also searches for a file in the same directory, with the same name + * but suffixed by ".key", containing the content key. If such file does not exist, no content key is stored. + * + * @param path the file to read from, in ZIP format + * @return a {@link ResourcePack} representation + * @throws IllegalArgumentException if the pack manifest was invalid or there was any processing exception + */ + public static GeyserResourcePack readPack(Path path) throws IllegalArgumentException { + if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { + throw new IllegalArgumentException("Resource pack " + path.getFileName() + " must be a .zip or .mcpack file!"); + } + + AtomicReference manifestReference = new AtomicReference<>(); + + try (ZipFile zip = new ZipFile(path.toFile()); + Stream stream = zip.stream()) { + stream.forEach(x -> { + String name = x.getName(); + if (SHOW_RESOURCE_PACK_LENGTH_WARNING && name.length() >= 80) { + GeyserImpl.getInstance().getLogger().warning("The resource pack " + path.getFileName() + + " has a file in it that meets or exceeds 80 characters in its path (" + name + + ", " + name.length() + " characters long). This will cause problems on some Bedrock platforms." + + " Please rename it to be shorter, or reduce the amount of folders needed to get to the file."); + } + if (name.contains("manifest.json")) { + try { + GeyserResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), GeyserResourcePackManifest.class); + if (manifest.header().uuid() != null) { + manifestReference.set(manifest); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + + GeyserResourcePackManifest manifest = manifestReference.get(); + if (manifest == null) { + throw new IllegalArgumentException(path.getFileName() + " does not contain a valid pack_manifest.json or manifest.json"); + } + + // Check if a file exists with the same name as the resource pack suffixed by .key, + // and set this as content key. (e.g. test.zip, key file would be test.zip.key) + Path keyFile = path.resolveSibling(path.getFileName().toString() + ".key"); + String contentKey = Files.exists(keyFile) ? Files.readString(path, StandardCharsets.UTF_8) : ""; + + return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); + } catch (Exception e) { + throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path.getFileName()), e); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java b/core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java index 999df0929..58c8f0072 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java +++ b/core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java @@ -30,10 +30,9 @@ import org.geysermc.geyser.util.AssetUtils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import java.util.UUID; @@ -80,14 +79,14 @@ public final class ProvidedSkins { .resolve(slim ? "slim" : "wide"); String assetName = asset.substring(asset.lastIndexOf('/') + 1); - File location = folder.resolve(assetName).toFile(); - AssetUtils.addTask(!location.exists(), new AssetUtils.ClientJarTask("assets/minecraft/" + asset, + Path location = folder.resolve(assetName); + AssetUtils.addTask(!Files.exists(location), new AssetUtils.ClientJarTask("assets/minecraft/" + asset, (stream) -> AssetUtils.saveFile(location, stream), () -> { try { // TODO lazy initialize? BufferedImage image; - try (InputStream stream = new FileInputStream(location)) { + try (InputStream stream = Files.newInputStream(location)) { image = ImageIO.read(stream); } diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java index fa048cecf..260b45136 100644 --- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java @@ -34,6 +34,7 @@ import org.geysermc.geyser.util.WebUtils; import javax.annotation.Nullable; import java.io.*; import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; @@ -57,8 +58,8 @@ public class MinecraftLocale { } public static void ensureEN_US() { - File localeFile = getFile("en_us"); - AssetUtils.addTask(!localeFile.exists(), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json", + Path localeFile = getPath("en_us"); + AssetUtils.addTask(!Files.exists(localeFile), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json", (stream) -> AssetUtils.saveFile(localeFile, stream), () -> { if ("en_us".equals(GeyserLocale.getDefaultLocale())) { @@ -106,10 +107,10 @@ public class MinecraftLocale { if (locale.equals("en_us")) { return; } - File localeFile = getFile(locale); + Path localeFile = getPath(locale); // Check if we have already downloaded the locale file - if (localeFile.exists()) { + if (Files.exists(localeFile)) { String curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile)); String targetHash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash(); @@ -130,8 +131,8 @@ public class MinecraftLocale { } } - private static File getFile(String locale) { - return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile(); + private static Path getPath(String locale) { + return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json"); } /** diff --git a/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java b/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java index 299e63e0e..6bdae6dfe 100644 --- a/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java @@ -192,8 +192,8 @@ public final class AssetUtils { } } - public static void saveFile(File location, InputStream fileStream) throws IOException { - try (FileOutputStream outStream = new FileOutputStream(location)) { + public static void saveFile(Path location, InputStream fileStream) throws IOException { + try (OutputStream outStream = Files.newOutputStream(location)) { // Write the file to the locale dir byte[] buf = new byte[fileStream.available()]; diff --git a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java index 6df9c2177..2fde1547d 100644 --- a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java @@ -129,14 +129,14 @@ public class FileUtils { /** * Calculate the SHA256 hash of a file * - * @param file File to calculate the hash for + * @param path Path to calculate the hash for * @return A byte[] representation of the hash */ - public static byte[] calculateSHA256(File file) { + public static byte[] calculateSHA256(Path path) { byte[] sha256; try { - sha256 = MessageDigest.getInstance("SHA-256").digest(readAllBytes(file)); + sha256 = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)); } catch (Exception e) { throw new RuntimeException("Could not calculate pack hash", e); } @@ -147,14 +147,14 @@ public class FileUtils { /** * Calculate the SHA1 hash of a file * - * @param file File to calculate the hash for + * @param path Path to calculate the hash for * @return A byte[] representation of the hash */ - public static byte[] calculateSHA1(File file) { + public static byte[] calculateSHA1(Path path) { byte[] sha1; try { - sha1 = MessageDigest.getInstance("SHA-1").digest(readAllBytes(file)); + sha1 = MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(path)); } catch (Exception e) { throw new RuntimeException("Could not calculate pack hash", e); } @@ -162,20 +162,6 @@ public class FileUtils { return sha1; } - /** - * An android compatible version of {@link Files#readAllBytes} - * - * @param file File to read bytes of - * @return The byte array of the file - */ - public static byte[] readAllBytes(File file) { - try (InputStream stream = new FileInputStream(file)) { - return stream.readAllBytes(); - } catch (IOException e) { - throw new RuntimeException("Cannot read " + file); - } - } - /** * @param resource the internal resource to read off from * @return the byte array of an InputStream diff --git a/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java b/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java new file mode 100644 index 000000000..8150ac446 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.loader; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.nio.file.PathMatcher; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ResourcePackLoaderTest { + + @Test + public void testPathMatcher() { + PathMatcher matcher = ResourcePackLoader.PACK_MATCHER; + + assertTrue(matcher.matches(Path.of("pack.mcpack"))); + assertTrue(matcher.matches(Path.of("pack.zip"))); + assertTrue(matcher.matches(Path.of("packs", "pack.mcpack"))); + assertTrue(matcher.matches(Path.of("packs", "category", "pack.mcpack"))); + + assertTrue(matcher.matches(Path.of("packs", "Resource+Pack+1.2.3.mcpack"))); + assertTrue(matcher.matches(Path.of("Resource+Pack+1.2.3.mcpack"))); + + assertTrue(matcher.matches(Path.of("packs", "Resource+Pack+1.2.3.zip"))); + assertTrue(matcher.matches(Path.of("Resource+Pack+1.2.3.zip"))); + + assertFalse(matcher.matches(Path.of("resource.pack"))); + assertFalse(matcher.matches(Path.of("pack.7zip"))); + assertFalse(matcher.matches(Path.of("packs"))); + } +}