3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-10-03 16:31:14 +02:00

Merge remote-tracking branch 'refs/remotes/origin/rp' into subpacks-rewrite-merge-urlpacks

# Conflicts:
#	api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java
#	core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java
#	core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
#	core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java
#	core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
#	core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java
Dieser Commit ist enthalten in:
onebeastchris 2024-08-20 20:06:34 +02:00
Commit 9279c70624
19 geänderte Dateien mit 748 neuen und 47 gelöschten Zeilen

Datei anzeigen

@ -45,7 +45,7 @@ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent {
} }
/** /**
* Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. * 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. * @return an unmodifiable list of resource packs that will be sent to the client.
*/ */

Datei anzeigen

@ -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.event.lifecycle;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.event.Event;
import org.geysermc.geyser.api.pack.ResourcePack;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
/**
* Called when {@link ResourcePack}'s are loaded within Geyser.
*/
public abstract class GeyserDefineResourcePacksEvent implements Event {
/**
* Gets an unmodifiable list of {@link ResourcePack}'s that will be sent to clients.
*
* @return an unmodifiable list of resource packs that will be sent to clients.
*/
public abstract @NonNull List<ResourcePack> resourcePacks();
/**
* Registers a {@link ResourcePack} to be sent to clients.
*
* @param resourcePack a resource pack that will be sent to clients.
* @return true if the resource pack was added successfully,
* or false if already present
*/
public abstract boolean register(@NonNull ResourcePack resourcePack);
/**
* Registers a collection of {@link ResourcePack}'s to be sent to clients.
*
* @param resourcePacks a collection of resource pack's that will be sent to clients.
*/
public abstract void registerAll(@NonNull Collection<ResourcePack> resourcePacks);
/**
* Unregisters a {@link ResourcePack} from being sent to clients.
*
* @param uuid the UUID of the resource pack to remove.
* @return true whether the resource pack was removed successfully.
*/
public abstract boolean unregister(@NonNull UUID uuid);
}

Datei anzeigen

@ -33,8 +33,11 @@ import java.util.List;
/** /**
* Called when resource packs are loaded within Geyser. * Called when resource packs are loaded within Geyser.
* @deprecated Use {@link GeyserDefineResourcePacksEvent} instead.
* *
* @param resourcePacks a mutable list of the currently listed resource packs * @param resourcePacks a mutable list of the currently listed resource packs
*/ */
@Deprecated
public record GeyserLoadResourcePacksEvent(@NonNull List<Path> resourcePacks) implements Event { public record GeyserLoadResourcePacksEvent(@NonNull List<Path> resourcePacks) implements Event {
} }

Datei anzeigen

@ -26,6 +26,7 @@
package org.geysermc.geyser.api.pack; package org.geysermc.geyser.api.pack;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.GeyserApi;
import java.io.IOException; import java.io.IOException;
@ -94,4 +95,27 @@ public abstract class PackCodec {
public static PackCodec path(@NonNull Path path) { public static PackCodec path(@NonNull Path path) {
return GeyserApi.api().provider(PathPackCodec.class, path); return GeyserApi.api().provider(PathPackCodec.class, path);
} }
/**
* Creates a new pack provider from the given url with no content key.
*
* @param url the url to create the pack provider from
* @return the new pack provider
*/
@NonNull
public static PackCodec url(@NonNull String url) {
return url(url, null);
}
/**
* Creates a new pack provider from the given url and content key.
*
* @param url the url to create the pack provider from
* @param contentKey the content key, leave empty or null if pack is not encrypted
* @return the new pack provider
*/
@NonNull
public static PackCodec url(@NonNull String url, @Nullable String contentKey) {
return GeyserApi.api().provider(UrlPackCodec.class, url, contentKey);
}
} }

Datei anzeigen

@ -0,0 +1,58 @@
/*
* 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 pack codec that creates a resource
* pack from a URL.
* <p>
* Due to Bedrock limitations, the URL must:
* <ul>
* <li>be a direct download link to a .zip or .mcpack resource pack</li>
* <li>use the application type `application/zip` and set a correct content length</li>
* </ul>
*/
public abstract class UrlPackCodec extends PackCodec {
/**
* Gets the URL to the resource pack location.
*
* @return the URL of the resource pack
*/
@NonNull
public abstract String url();
/**
* If the remote pack has an encryption key, it must be specified here.
* This will return empty if none is specified.
*
* @return the encryption key of the resource pack
*/
@NonNull
public abstract String contentKey();
}

Datei anzeigen

@ -81,6 +81,7 @@ import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.network.netty.GeyserServer;
import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.loader.ResourcePackLoader;
import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.registry.provider.ProviderSupplier;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
@ -719,7 +720,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
runIfNonNull(newsHandler, NewsHandler::shutdown); runIfNonNull(newsHandler, NewsHandler::shutdown);
runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); runIfNonNull(erosionUnixListener, UnixSocketClientListener::close);
Registries.RESOURCE_PACKS.get().clear(); ResourcePackLoader.clear();
this.setEnabled(false); this.setEnabled(false);
} }

Datei anzeigen

@ -96,6 +96,8 @@ public interface GeyserConfiguration {
boolean isForceResourcePacks(); boolean isForceResourcePacks();
List<String> getResourcePackUrls();
@SuppressWarnings("BooleanMethodIsAlwaysInverted") @SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isXboxAchievementsEnabled(); boolean isXboxAchievementsEnabled();

Datei anzeigen

@ -136,6 +136,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("force-resource-packs") @JsonProperty("force-resource-packs")
private boolean forceResourcePacks = true; private boolean forceResourcePacks = true;
@JsonProperty("resource-pack-urls")
private List<String> resourcePackUrls = new ArrayList<>();
@JsonProperty("xbox-achievements-enabled") @JsonProperty("xbox-achievements-enabled")
private boolean xboxAchievementsEnabled = false; private boolean xboxAchievementsEnabled = false;

Datei anzeigen

@ -0,0 +1,71 @@
/*
* 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 lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent;
import org.geysermc.geyser.api.pack.ResourcePack;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Getter
public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent {
private final Map<String, ResourcePack> packs;
public GeyserDefineResourcePacksEventImpl(Map<String, ResourcePack> packMap) {
this.packs = packMap;
}
@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 void registerAll(@NonNull Collection<ResourcePack> resourcePacks) {
resourcePacks.forEach(this::register);
}
@Override
public boolean unregister(@NonNull UUID uuid) {
return packs.remove(uuid.toString()) != null;
}
}

Datei anzeigen

@ -59,10 +59,12 @@ import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.PackCodec;
import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.api.pack.ResourcePackManifest;
import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl;
import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.loader.ResourcePackLoader;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
@ -80,7 +82,7 @@ import java.util.OptionalInt;
public class UpstreamPacketHandler extends LoggingPacketHandler { public class UpstreamPacketHandler extends LoggingPacketHandler {
private boolean networkSettingsRequested = false; private boolean networkSettingsRequested = false;
private final Deque<String> packsToSent = new ArrayDeque<>(); private final Deque<String> packsToSend = new ArrayDeque<>();
private final CompressionStrategy compressionStrategy; private final CompressionStrategy compressionStrategy;
private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; private SessionLoadResourcePacksEventImpl resourcePackLoadEvent;
@ -204,6 +206,14 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries()); resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries());
// TODO add url pack entries
/*
if (pack.codec() instanceof UrlPackCodec urlPackCodec) {
resourcePacksInfo.getCDNEntries().add(new ResourcePacksInfoPacket.CDNEntry(
header.uuid() + "_" + header.version(), urlPackCodec.url()));
}
*/
resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks());
session.sendUpstreamPacket(resourcePacksInfo); session.sendUpstreamPacket(resourcePacksInfo);
@ -214,7 +224,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override @Override
public PacketSignal handle(ResourcePackClientResponsePacket packet) { public PacketSignal handle(ResourcePackClientResponsePacket packet) {
switch (packet.getStatus()) { switch (packet.getStatus()) {
case COMPLETED: case COMPLETED -> {
if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) { if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) {
session.authenticate(session.getAuthData().name()); session.authenticate(session.getAuthData().name());
} else if (!couldLoginUserByName(session.getAuthData().name())) { } else if (!couldLoginUserByName(session.getAuthData().name())) {
@ -222,14 +232,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
session.connect(); session.connect();
} }
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.connect", session.getAuthData().name())); geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.connect", session.getAuthData().name()));
break; }
case SEND_PACKS -> {
case SEND_PACKS: packsToSend.addAll(packet.getPackIds());
packsToSent.addAll(packet.getPackIds()); sendPackDataInfo(packsToSend.pop());
sendPackDataInfo(packsToSent.pop()); }
break; case HAVE_ALL_PACKS -> {
case HAVE_ALL_PACKS:
ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); ResourcePackStackPacket stackPacket = new ResourcePackStackPacket();
stackPacket.setExperimentsPreviouslyToggled(false); stackPacket.setExperimentsPreviouslyToggled(false);
stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not
@ -245,11 +253,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
stackPacket.getExperiments().add(new ExperimentData("updateAnnouncedLive2023", true)); stackPacket.getExperiments().add(new ExperimentData("updateAnnouncedLive2023", true));
session.sendUpstreamPacket(stackPacket); session.sendUpstreamPacket(stackPacket);
break; }
default -> session.disconnect("disconnectionScreen.resourcePack");
default:
session.disconnect("disconnectionScreen.resourcePack");
break;
} }
return PacketSignal.HANDLED; return PacketSignal.HANDLED;
@ -309,6 +314,11 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket();
PackCodec codec = pack.codec(); PackCodec codec = pack.codec();
// If a remote pack ends up here, that usually implies that a client was not able to download the pack
if (codec instanceof UrlPackCodec urlPackCodec) {
ResourcePackLoader.testRemotePack(session, urlPackCodec, packet.getPackId().toString(), packet.getPackVersion());
}
data.setChunkIndex(packet.getChunkIndex()); data.setChunkIndex(packet.getChunkIndex());
data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE); data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE);
data.setPackVersion(packet.getPackVersion()); data.setPackVersion(packet.getPackVersion());
@ -330,8 +340,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
session.sendUpstreamPacket(data); session.sendUpstreamPacket(data);
// Check if it is the last chunk and send next pack in queue when available. // Check if it is the last chunk and send next pack in queue when available.
if (remainingSize <= GeyserResourcePack.CHUNK_SIZE && !packsToSent.isEmpty()) { if (remainingSize <= GeyserResourcePack.CHUNK_SIZE && !packsToSend.isEmpty()) {
sendPackDataInfo(packsToSent.pop()); sendPackDataInfo(packsToSend.pop());
} }
return PacketSignal.HANDLED; return PacketSignal.HANDLED;

Datei anzeigen

@ -36,10 +36,10 @@ import java.util.Collection;
import java.util.Objects; import java.util.Objects;
public record GeyserResourcePack( public record GeyserResourcePack(
PackCodec codec, @NonNull PackCodec codec,
ResourcePackManifest manifest, @NonNull ResourcePackManifest manifest,
String contentKey, @NonNull String contentKey,
OptionHolder options @NonNull OptionHolder options
) implements ResourcePack { ) implements ResourcePack {
/** /**

Datei anzeigen

@ -0,0 +1,103 @@
/*
* 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.url;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.pack.PathPackCodec;
import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.registry.loader.ResourcePackLoader;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.util.Objects;
public class GeyserUrlPackCodec extends UrlPackCodec {
private final @NonNull String url;
private final @Nullable String contentKey;
@Getter
private PathPackCodec fallback;
public GeyserUrlPackCodec(String url) throws IllegalArgumentException {
this(url, null);
}
public GeyserUrlPackCodec(@NonNull String url, @Nullable String contentKey) throws IllegalArgumentException {
Objects.requireNonNull(url, "url cannot be null");
this.url = url;
this.contentKey = contentKey;
}
@Override
public byte @NonNull [] sha256() {
Objects.requireNonNull(fallback, "must call #create() before attempting to get the sha256!");
return fallback.sha256();
}
@Override
public long size() {
Objects.requireNonNull(fallback, "must call #create() before attempting to get the size!");
return fallback.size();
}
@Override
public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException {
Objects.requireNonNull(fallback, "must call #create() before attempting to serialize!!");
return fallback.serialize(resourcePack);
}
@Override
@NonNull
public ResourcePack create() {
if (this.fallback == null) {
try {
ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> {
if (throwable != null) {
throw new IllegalArgumentException(throwable);
} else if (pack != null) {
this.fallback = pack;
}
}).join(); // Needed to ensure that we don't attempt to read a pack before downloading/checking it
} catch (Exception e) {
throw new IllegalArgumentException("Failed to download pack from the url %s (reason: %s)!".formatted(url, e.getMessage()));
}
}
return ResourcePackLoader.readPack(this);
}
@Override
public @NonNull String url() {
return this.url;
}
@Override
public @NonNull String contentKey() {
return this.contentKey != null ? contentKey : "";
}
}

Datei anzeigen

@ -40,6 +40,7 @@ import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.PathPackCodec;
import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.api.pack.option.PriorityOption; import org.geysermc.geyser.api.pack.option.PriorityOption;
import org.geysermc.geyser.api.pack.option.SubpackOption; import org.geysermc.geyser.api.pack.option.SubpackOption;
import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.event.GeyserEventRegistrar;
@ -58,6 +59,7 @@ import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData;
import org.geysermc.geyser.pack.option.GeyserPriorityOption; import org.geysermc.geyser.pack.option.GeyserPriorityOption;
import org.geysermc.geyser.pack.option.GeyserSubpackOption; import org.geysermc.geyser.pack.option.GeyserSubpackOption;
import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.pack.path.GeyserPathPackCodec;
import org.geysermc.geyser.pack.url.GeyserUrlPackCodec;
import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.registry.provider.ProviderSupplier;
import java.nio.file.Path; import java.nio.file.Path;
@ -88,6 +90,7 @@ public class ProviderRegistryLoader implements RegistryLoader<Map<Class<?>, Prov
providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0]));
providers.put(PriorityOption.class, args -> new GeyserPriorityOption((double) args[0])); providers.put(PriorityOption.class, args -> new GeyserPriorityOption((double) args[0]));
providers.put(SubpackOption.class, args -> new GeyserSubpackOption((String) args[0])); providers.put(SubpackOption.class, args -> new GeyserSubpackOption((String) args[0]));
providers.put(UrlPackCodec.class, args -> new GeyserUrlPackCodec((String) args[0]));
// items // items
providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.Builder()); providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.Builder());

Datei anzeigen

@ -25,27 +25,39 @@
package org.geysermc.geyser.registry.loader; package org.geysermc.geyser.registry.loader;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent;
import org.geysermc.geyser.api.pack.PathPackCodec;
import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.api.pack.ResourcePackManifest;
import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl;
import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.pack.GeyserResourcePackManifest; import org.geysermc.geyser.pack.GeyserResourcePackManifest;
import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.SkullResourcePackManager;
import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.pack.path.GeyserPathPackCodec;
import org.geysermc.geyser.pack.url.GeyserUrlPackCodec;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.PathMatcher; import java.nio.file.PathMatcher;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.concurrent.CompletableFuture;
import java.util.List; import java.util.concurrent.RejectedExecutionException;
import java.util.Map; import java.util.concurrent.TimeUnit;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -53,10 +65,18 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
/** /**
* Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserLoadResourcePacksEvent}. * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserDefineResourcePacksEventImpl}.
*/ */
public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, GeyserResourcePack>> { public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, GeyserResourcePack>> {
/**
* Used to keep track of remote resource packs that the client rejected.
* If a client rejects such a pack, it falls back to the old method, and Geyser serves a cached variant.
*/
private static final Cache<String, UrlPackCodec> CACHED_FAILED_PACKS = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); 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")); private static final boolean SHOW_RESOURCE_PACK_LENGTH_WARNING = Boolean.parseBoolean(System.getProperty("Geyser.ShowResourcePackLengthWarning", "true"));
@ -66,7 +86,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
*/ */
@Override @Override
public Map<UUID, GeyserResourcePack> load(Path directory) { public Map<UUID, GeyserResourcePack> load(Path directory) {
Map<UUID, GeyserResourcePack> packMap = new HashMap<>(); Map<UUID, GeyserResourcePack> packMap = new Object2ObjectOpenHashMap<>();
if (!Files.exists(directory)) { if (!Files.exists(directory)) {
try { try {
@ -95,6 +115,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
resourcePacks.add(skullResourcePack); resourcePacks.add(skullResourcePack);
} }
@SuppressWarnings("deprecation")
GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks);
GeyserImpl.getInstance().eventBus().fire(event); GeyserImpl.getInstance().eventBus().fire(event);
@ -106,7 +127,11 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
e.printStackTrace(); e.printStackTrace();
} }
} }
return packMap;
packMap.putAll(loadRemotePacks());
GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap);
GeyserImpl.getInstance().eventBus().fire(defineEvent);
return defineEvent.getPacks();
} }
/** /**
@ -118,10 +143,41 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
* @throws IllegalArgumentException if the pack manifest was invalid or there was any processing exception * @throws IllegalArgumentException if the pack manifest was invalid or there was any processing exception
*/ */
public static GeyserResourcePack.Builder readPack(Path path) throws IllegalArgumentException { public static GeyserResourcePack.Builder readPack(Path path) throws IllegalArgumentException {
if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { if (!PACK_MATCHER.matches(path)) {
throw new IllegalArgumentException("Resource pack " + path.getFileName() + " must be a .zip or .mcpack file!"); throw new IllegalArgumentException("Resource pack " + path.getFileName() + " must be a .zip or .mcpack file!");
} }
ResourcePackManifest manifest = readManifest(path, path.getFileName().toString());
String contentKey;
try {
// 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");
contentKey = Files.exists(keyFile) ? Files.readString(keyFile, StandardCharsets.UTF_8) : "";
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to read content key for resource pack " + path.getFileName(), e);
contentKey = "";
}
return new GeyserResourcePack.Builder(new GeyserPathPackCodec(path), manifest, contentKey);
}
/**
* Reads a Resource pack from a URL codec, and returns a resource pack. Unlike {@link ResourcePackLoader#readPack(Path)}
* this method reads content keys differently.
*
* @param codec the URL pack codec with the url to download the pack from
* @return a {@link GeyserResourcePack} representation
* @throws IllegalArgumentException if there was an error reading the pack.
*/
public static GeyserResourcePack readPack(GeyserUrlPackCodec codec) throws IllegalArgumentException {
Path path = codec.getFallback().path();
ResourcePackManifest manifest = readManifest(path, codec.url());
return new GeyserResourcePack(codec, manifest, codec.contentKey());
}
private static ResourcePackManifest readManifest(Path path, String packLocation) throws IllegalArgumentException {
AtomicReference<GeyserResourcePackManifest> manifestReference = new AtomicReference<>(); AtomicReference<GeyserResourcePackManifest> manifestReference = new AtomicReference<>();
try (ZipFile zip = new ZipFile(path.toFile()); try (ZipFile zip = new ZipFile(path.toFile());
@ -129,7 +185,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
stream.forEach(x -> { stream.forEach(x -> {
String name = x.getName(); String name = x.getName();
if (SHOW_RESOURCE_PACK_LENGTH_WARNING && name.length() >= 80) { if (SHOW_RESOURCE_PACK_LENGTH_WARNING && name.length() >= 80) {
GeyserImpl.getInstance().getLogger().warning("The resource pack " + path.getFileName() GeyserImpl.getInstance().getLogger().warning("The resource pack " + packLocation
+ " has a file in it that meets or exceeds 80 characters in its path (" + name + " 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." + + ", " + 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."); " Please rename it to be shorter, or reduce the amount of folders needed to get to the file.");
@ -148,17 +204,192 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Geyser
GeyserResourcePackManifest manifest = manifestReference.get(); GeyserResourcePackManifest manifest = manifestReference.get();
if (manifest == null) { if (manifest == null) {
throw new IllegalArgumentException(path.getFileName() + " does not contain a valid pack_manifest.json or manifest.json"); throw new IllegalArgumentException(packLocation + " 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, return manifest;
// 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(keyFile, StandardCharsets.UTF_8) : "";
return new GeyserResourcePack.Builder(new GeyserPathPackCodec(path), manifest, contentKey);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path.getFileName()), e); throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", packLocation), e);
}
}
private Map<String, ResourcePack> loadRemotePacks() {
GeyserImpl instance = GeyserImpl.getInstance();
// Unable to make this a static variable, as the test would fail
final Path cachedCdnPacksDirectory = instance.getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs");
if (!Files.exists(cachedCdnPacksDirectory)) {
try {
Files.createDirectories(cachedCdnPacksDirectory);
} catch (IOException e) {
instance.getLogger().error("Could not create remote pack cache directory", e);
return new Object2ObjectOpenHashMap<>();
}
}
List<String> remotePackUrls = instance.getConfig().getResourcePackUrls();
Map<String, ResourcePack> packMap = new Object2ObjectOpenHashMap<>();
for (String url : remotePackUrls) {
try {
GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url);
ResourcePack pack = codec.create();
packMap.put(pack.manifest().header().uuid().toString(), pack);
} catch (Throwable e) {
instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url));
instance.getLogger().error(e.getMessage());
if (instance.getLogger().isDebug()) {
e.printStackTrace();
}
}
}
// After loading the new resource packs: let's clean up the old
cleanupRemotePacks();
return packMap;
}
/**
* Used when a Bedrock client requests a Bedrock resource pack from the server when it should be downloading it
* from a remote provider. Since this would be called each time a Bedrock client requests a piece of the Bedrock pack,
* this uses a cache to ensure we aren't re-checking a dozen times.
*
* @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client.
*/
public static void testRemotePack(GeyserSession session, UrlPackCodec codec, String packId, String packVersion) {
if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) {
String url = codec.url();
CACHED_FAILED_PACKS.put(url, codec);
GeyserImpl.getInstance().getLogger().warning(
"A Bedrock client (%s, playing on %s / %s) was not able to download the resource pack at %s. Checking for changes now:"
.formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), session.getClientData().getDeviceId(), codec.url())
);
downloadPack(codec.url(), true).whenComplete((pathPackCodec, e) -> {
if (e != null) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e);
if (GeyserImpl.getInstance().getLogger().isDebug()) {
e.printStackTrace();
if (pathPackCodec != null) {
deleteFile(pathPackCodec.path());
}
return;
}
}
if (pathPackCodec == null) {
return; // Already warned about
}
ResourcePack newPack = ResourcePackLoader.readPack(pathPackCodec.path());
UUID newUUID = newPack.manifest().header().uuid();
if (newUUID.toString().equals(packId)) {
if (packVersion.equals(newPack.manifest().header().version().toString())) {
GeyserImpl.getInstance().getLogger().info("No version or pack change detected: Was the resource pack server down?");
} else {
GeyserImpl.getInstance().getLogger().info("Detected a new resource pack version (%s, old version %s) for pack at %s!"
.formatted(packVersion, newPack.manifest().header().version().toString(), url));
}
} else {
GeyserImpl.getInstance().getLogger().info("Detected a new resource pack at the url %s!".formatted(url));
}
// This should be safe to do as we're not directly using registries to read packs.
// Instead, they're cached per-session in the SessionLoadResourcePacks event
Registries.RESOURCE_PACKS.get().remove(packId);
Registries.RESOURCE_PACKS.get().put(newUUID.toString(), newPack);
if (codec instanceof GeyserUrlPackCodec geyserUrlPackCodec && geyserUrlPackCodec.getFallback() != null) {
// Other implementations could, in theory, not have a fallback
Path path = geyserUrlPackCodec.getFallback().path();
try {
GeyserImpl.getInstance().getScheduledThread().schedule(() -> {
CACHED_FAILED_PACKS.invalidate(packId);
deleteFile(path);
}, 5, TimeUnit.MINUTES);
} catch (RejectedExecutionException exception) {
// No scheduling here, probably because we're shutting down?
deleteFile(path);
}
}
});
}
}
private static void deleteFile(Path path) {
if (path.toFile().exists()) {
try {
Files.delete(path);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Unable to delete old pack! " + e.getMessage());
e.printStackTrace();
}
}
}
public static CompletableFuture<@Nullable PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException {
return CompletableFuture.supplyAsync(() -> {
Path path = WebUtils.downloadRemotePack(url, testing);
// Already warned about these above
if (path == null) {
return null;
}
// Check if the pack is a .zip or .mcpack file
if (!PACK_MATCHER.matches(path)) {
throw new IllegalArgumentException("Invalid pack format from url %s! Not a .zip or .mcpack file.".formatted(url));
}
try {
try (ZipFile zip = new ZipFile(path.toFile())) {
if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) {
throw new IllegalArgumentException("The pack at the url " + url + " does not contain a manifest file!");
}
// Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise.
// (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json)
if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) {
if (GeyserImpl.getInstance().getLogger().isDebug()) {
GeyserImpl.getInstance().getLogger().info("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " +
"This may not work for remote packs, and could cause Bedrock clients to fall back to request the pack from the server. " +
"Please put the pack file in a subfolder, and provide that zip in the URL.");
}
}
}
} catch (IOException e) {
throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e);
}
return new GeyserPathPackCodec(path);
});
}
public static void clear() {
Registries.RESOURCE_PACKS.get().clear();
CACHED_FAILED_PACKS.invalidateAll();
}
public static void cleanupRemotePacks() {
File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").toFile();
if (!cacheFolder.exists()) {
return;
}
int count = 0;
final long expireTime = (((long) 1000 * 60 * 60)); // one hour
for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) {
if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) {
//noinspection ResultOfMethodCallIgnored
imageFile.delete();
count++;
}
}
if (count > 0) {
GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached resource pack files as they are no longer in use!", count));
} }
} }
} }

Datei anzeigen

@ -51,7 +51,7 @@ import java.util.stream.Collectors;
import static org.geysermc.geyser.scoreboard.UpdateType.*; import static org.geysermc.geyser.scoreboard.UpdateType.*;
public final class Scoreboard { public final class Scoreboard {
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true")); private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "false"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true")); private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
private final GeyserSession session; private final GeyserSession session;

Datei anzeigen

@ -48,7 +48,7 @@ public final class ScoreboardUpdater extends Thread {
static { static {
GeyserConfiguration config = GeyserImpl.getInstance().getConfig(); GeyserConfiguration config = GeyserImpl.getInstance().getConfig();
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
DEBUG_ENABLED = config.isDebugMode(); DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "false")) && config.isDebugMode();
} }
private final GeyserImpl geyser = GeyserImpl.getInstance(); private final GeyserImpl geyser = GeyserImpl.getInstance();

Datei anzeigen

@ -167,7 +167,7 @@ public class SkinProvider {
if (count > 0) { if (count > 0) {
GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count)); GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count));
} }
}, 10, 1440, TimeUnit.MINUTES); }, 10, 1, TimeUnit.DAYS);
} }
} }

Datei anzeigen

@ -28,22 +28,26 @@ package org.geysermc.geyser.util;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import javax.naming.directory.Attribute; import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.*;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
public class WebUtils { public class WebUtils {
private static final Path REMOTE_PACK_CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs");
/** /**
* Makes a web request to the given URL and returns the body as a string * Makes a web request to the given URL and returns the body as a string
* *
@ -96,6 +100,114 @@ public class WebUtils {
} }
} }
/**
* Checks a remote pack URL to see if it is valid
* If it is, it will download the pack file and return a path to it
*
* @param url The URL to check
* @param force If true, the pack will be downloaded even if it is cached to a separate location.
* @return Path to the downloaded pack file, or null if it was unable to be loaded
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public static @Nullable Path downloadRemotePack(String url, boolean force) {
GeyserLogger logger = GeyserImpl.getInstance().getLogger();
try {
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION);
con.setInstanceFollowRedirects(true);
int responseCode = con.getResponseCode();
if (responseCode >= 400) {
throw new IllegalStateException(String.format("Invalid response code from remote pack at URL: %s (code: %d)", url, responseCode));
}
int size = con.getContentLength();
String type = con.getContentType();
if (size <= 0) {
throw new IllegalArgumentException(String.format("Invalid content length received from remote pack at URL: %s (size: %d)", url, size));
}
// This doesn't seem to be a requirement (anymore?). Logging to debug as it might be interesting though.
if (type == null || !type.equals("application/zip")) {
logger.warning(String.format("Application type received from remote pack at URL %s uses the content type: %s! This may result in packs not loading " +
"for Bedrock players.", url, type));
}
Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata");
Path downloadLocation;
// If we downloaded this pack before, reuse it if the ETag matches.
if (Files.exists(packMetadata) && !force) {
try {
List<String> metadata = Files.readAllLines(packMetadata, StandardCharsets.UTF_8);
int cachedSize = Integer.parseInt(metadata.get(0));
String cachedEtag = metadata.get(1);
long cachedLastModified = Long.parseLong(metadata.get(2));
downloadLocation = REMOTE_PACK_CACHE.resolve(metadata.get(3));
if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) &&
cachedLastModified == con.getLastModified() && downloadLocation.toFile().exists()) {
logger.debug("Using cached pack (%s) for %s.".formatted(downloadLocation.getFileName(), url));
downloadLocation.toFile().setLastModified(System.currentTimeMillis());
packMetadata.toFile().setLastModified(System.currentTimeMillis());
return downloadLocation;
}
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata! " + e);
try {
Files.delete(packMetadata);
} catch (Exception exception) {
GeyserImpl.getInstance().getLogger().error("Failed to delete pack metadata!", exception);
}
}
}
downloadLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + "_" + System.currentTimeMillis() + ".zip");
Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING);
// This needs to match as the client fails to download the pack otherwise
long downloadSize = Files.size(downloadLocation);
if (downloadSize != size) {
Files.delete(downloadLocation);
throw new IllegalStateException("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!"
.formatted(url, downloadSize, size));
}
try {
Files.write(
packMetadata,
Arrays.asList(
String.valueOf(size),
con.getHeaderField("ETag"),
String.valueOf(con.getLastModified()),
downloadLocation.getFileName().toString()
));
packMetadata.toFile().setLastModified(System.currentTimeMillis());
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage());
Files.delete(packMetadata);
Files.delete(downloadLocation);
return null;
}
downloadLocation.toFile().setLastModified(System.currentTimeMillis());
return downloadLocation;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s! ".formatted(url));
} catch (SocketTimeoutException | ConnectException e) {
logger.error("Unable to download pack from url %s due to network error! ( %s )".formatted(url, e.getMessage()));
logger.debug(e);
} catch (IOException e) {
throw new IllegalStateException("Unable to download and save remote resource pack from: %s ( %s )!".formatted(url, e.getMessage()));
}
return null;
}
/** /**
* Post a string to the given URL * Post a string to the given URL
* *

Datei anzeigen

@ -167,6 +167,14 @@ above-bedrock-nether-building: false
# want to download the resource packs. # want to download the resource packs.
force-resource-packs: true force-resource-packs: true
# A list of links to send to the client to download resource packs from.
# These must be direct links to the resource pack, not a link to a page containing the resource pack.
# If you enter a link here, Geyser will download the resource pack once to check if it's in a valid format.
# See https://wiki.geysermc.org/geyser/packs for more info.
resource-pack-urls:
# GeyserOptionalPack
- "https://download.geysermc.org/v2/projects/geyseroptionalpack/versions/latest/builds/latest/downloads/geyseroptionalpack"
# Allows Xbox achievements to be unlocked. # Allows Xbox achievements to be unlocked.
xbox-achievements-enabled: false xbox-achievements-enabled: false