Exposing resourcepack loading to api (#3696)

Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com>
Co-authored-by: RednedEpic <redned235@gmail.com>
This commit is contained in:
chris 2023-06-17 03:39:53 +02:00 committed by GitHub
parent 6eca6ade06
commit 903e61f1a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1334 additions and 347 deletions

View file

@ -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.
*

View file

@ -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<ResourcePack> 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);
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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
* <p>
* 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();
}
}

View file

@ -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<? extends Module> modules();
/**
* Gets the dependencies of the resource pack.
*
* @return the dependencies
*/
@NonNull
Collection<? extends Dependency> 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();
}
}

View file

@ -30,7 +30,7 @@ dependencies {
}
implementation(libs.raknet) {
exclude("io.netty", "*");
exclude("io.netty", "*")
}
implementation(libs.netty.resolver.dns)

View file

@ -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;

View file

@ -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<String, ResourcePack> packs;
public SessionLoadResourcePacksEventImpl(GeyserSession session, Map<String, ResourcePack> packMap) {
super(session);
this.packs = packMap;
}
public @NonNull Map<String, ResourcePack> getPacks() {
return packs;
}
@Override
public @NonNull List<ResourcePack> 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;
}
}

View file

@ -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<String> packsToSent = new ArrayDeque<>();
private final Deque<String> 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);

View file

@ -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;
}

View file

@ -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<Module> modules, Collection<Dependency> 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<Version> {
@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]);
}
}
}
}

View file

@ -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<String, ResourcePack> 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<Path> 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<? extends ZipEntry> 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;
}
}

View file

@ -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<Module> modules;
protected Collection<Dependency> dependencies;
public Collection<Module> 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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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.
* <p>
* 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 <M> the value being held by the registry
*/
public final class DeferredRegistry<M> implements IRegistry<M> {
private final Registry<M> backingRegistry;
private final Supplier<M> loader;
private boolean loaded;
private <I> DeferredRegistry(Function<RegistryLoader<I, M>, Registry<M>> registryLoader, RegistryLoader<I, M> deferredLoader) {
this.backingRegistry = registryLoader.apply(RegistryLoaders.uninitialized());
this.loader = () -> deferredLoader.load(null);
}
private <I> DeferredRegistry(Function<RegistryLoader<I, M>, Registry<M>> registryLoader, Supplier<RegistryLoader<I, M>> deferredLoader) {
this.backingRegistry = registryLoader.apply(RegistryLoaders.uninitialized());
this.loader = () -> deferredLoader.get().load(null);
}
private <I> DeferredRegistry(I input, RegistryInitializer<M> registryInitializer, RegistryLoader<I, M> deferredLoader) {
this.backingRegistry = registryInitializer.initialize(input, RegistryLoaders.uninitialized());
this.loader = () -> deferredLoader.load(input);
}
private <I> DeferredRegistry(I input, RegistryInitializer<M> registryInitializer, Supplier<RegistryLoader<I, M>> 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<M> 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 <I> the input type
* @param <M> the registry type
* @return the new deferred registry
*/
public static <I, M> DeferredRegistry<M> create(Function<RegistryLoader<I, M>, Registry<M>> registryLoader, RegistryLoader<I, M> deferredLoader) {
return new DeferredRegistry<>(registryLoader, deferredLoader);
}
/**
* Creates a new deferred registry.
*
* @param registryLoader the registry loader
* @param deferredLoader the deferred loader
* @param <I> the input type
* @param <M> the registry type
* @return the new deferred registry
*/
public static <I, M> DeferredRegistry<M> create(Function<RegistryLoader<I, M>, Registry<M>> registryLoader, Supplier<RegistryLoader<I, M>> deferredLoader) {
return new DeferredRegistry<>(registryLoader, deferredLoader);
}
/**
* Creates a new deferred registry.
*
* @param registryInitializer the registry initializer
* @param deferredLoader the deferred loader
* @param <I> the input type
* @param <M> the registry type
* @return the new deferred registry
*/
public static <I, M> DeferredRegistry<M> create(I input, RegistryInitializer<M> registryInitializer, RegistryLoader<I, M> deferredLoader) {
return new DeferredRegistry<>(input, registryInitializer, deferredLoader);
}
/**
* Creates a new deferred registry.
*
* @param registryInitializer the registry initializer
* @param deferredLoader the deferred loader
* @param <I> the input type
* @param <M> the registry type
* @return the new deferred registry
*/
public static <I, M> DeferredRegistry<M> create(I input, RegistryInitializer<M> registryInitializer, Supplier<RegistryLoader<I, M>> deferredLoader) {
return new DeferredRegistry<>(input, registryInitializer, deferredLoader);
}
/**
* A registry initializer.
*
* @param <M> the registry type
*/
interface RegistryInitializer<M> {
/**
* Initializes the registry.
*
* @param input the input
* @param registryLoader the registry loader
* @param <I> the input type
* @return the initialized registry
*/
<I> Registry<M> initialize(I input, RegistryLoader<I, M> registryLoader);
}
}

View file

@ -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 <M> the value being held by the registry
*/
interface IRegistry<M> {
/**
* 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<M> consumer);
}

View file

@ -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<org.cloudburstmc.protocol.bedrock.data.SoundEvent> RECORDS = IntMappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
/**
* A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys.
*/
public static final DeferredRegistry<Map<String, ResourcePack>> RESOURCE_PACKS = DeferredRegistry.create(GeyserImpl.getInstance().packDirectory(), SimpleMappedRegistry::create, RegistryLoaders.RESOURCE_PACKS);
/**
* A mapped registry holding sound identifiers to their corresponding {@link SoundMapping}.
*/

View file

@ -64,7 +64,7 @@ import java.util.function.Consumer;
*
* @param <M> the value being held by the registry
*/
public abstract class Registry<M> {
public abstract class Registry<M> implements IRegistry<M> {
protected M mappings;
/**
@ -85,6 +85,7 @@ public abstract class Registry<M> {
*
* @return the underlying value held by this registry.
*/
@Override
public M get() {
return this.mappings;
}
@ -96,6 +97,7 @@ public abstract class Registry<M> {
*
* @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<M> {
*
* @param consumer the consumer
*/
@Override
public void register(Consumer<M> consumer) {
consumer.accept(this.mappings);
}

View file

@ -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<Map<Class<?>, Prov
@Override
public Map<Class<?>, ProviderSupplier> load(Map<Class<?>, 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;
}

View file

@ -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 <I> the input
* @param <V> the value
* @return a RegistryLoader that is yet to contain a value.
*/
public static <V> RegistryLoader<Object, V> uninitialized() {
public static <I, V> RegistryLoader<I, V> uninitialized() {
return input -> null;
}

View file

@ -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<Path, Map<String, ResourcePack>> {
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<String, ResourcePack> load(Path directory) {
Map<String, ResourcePack> 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<Path> resourcePacks;
try (Stream<Path> 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<GeyserResourcePackManifest> manifestReference = new AtomicReference<>();
try (ZipFile zip = new ZipFile(path.toFile());
Stream<? extends ZipEntry> 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);
}
}
}

View file

@ -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);
}

View file

@ -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");
}
/**

View file

@ -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()];

View file

@ -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

View file

@ -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")));
}
}