1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

Add automatic loading of the GeyserOptionalPack (feature/configurate) (#5964)

* Add automatic downloading of the GeyserOptionalPack

* Warn about including the OptionalPack from extensions when Geyser is already including it instead of throwing.

* Copy optional pack instead of downloading

---------

Co-authored-by: onebeastchris <github@onechris.mozmail.com>
This commit is contained in:
Auri
2025-11-06 17:50:17 +00:00
committed by GitHub
parent 2388ed9045
commit b5ec3e9581
9 changed files with 174 additions and 13 deletions

View File

@@ -251,12 +251,10 @@ public interface GeyserConfig {
String serverName();
@Comment("""
Whether to automatically serve the GeyserOptionalPack to all connecting players.
This adds some quality-of-life visual fixes for Bedrock players.
See https://geysermc.org/wiki/other/geyseroptionalpack for all current features.
Whether to automatically serve a resource pack that is required for some Geyser features to all connecting Bedrock players.
If enabled, force-resource-packs will be enabled.""")
@DefaultBoolean(true)
boolean enableOptionalPack();
boolean enableIntegratedPack();
@Comment("""
Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat.
@@ -439,6 +437,14 @@ public interface GeyserConfig {
@DefaultBoolean(true)
boolean addTeamSuggestions();
@Comment("""
A list of remote resource pack urls to send to the Bedrock client for downloading.
The Bedrock client is very picky about how these are delivered - please see our wiki page for further info: https://geysermc.org/wiki/geyser/packs/
""")
default List<String> resourcePackUrls() {
return Collections.emptyList();
}
// Cannot be type File yet because we may want to hide it in plugin instances.
@Comment("""
Floodgate uses encryption to ensure use from authorized sources.

View File

@@ -34,6 +34,7 @@ import org.geysermc.geyser.api.pack.exception.ResourcePackException;
import org.geysermc.geyser.api.pack.option.ResourcePackOption;
import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.util.GeyserIntegratedPackUtil;
import java.util.Collection;
import java.util.List;
@@ -42,11 +43,12 @@ import java.util.Objects;
import java.util.UUID;
@Getter
public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent {
public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent implements GeyserIntegratedPackUtil {
private final Map<UUID, ResourcePackHolder> packs;
public GeyserDefineResourcePacksEventImpl(Map<UUID, ResourcePackHolder> packMap) {
this.packs = packMap;
registerGeyserPack(this);
}
@Override
@@ -61,6 +63,10 @@ public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePack
throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION);
}
if (handlePossibleOptionalPack(resourcePack)) {
return;
}
UUID uuid = resourcePack.uuid();
if (packs.containsKey(uuid)) {
throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE);

View File

@@ -45,6 +45,7 @@ import org.geysermc.geyser.pack.ResourcePackHolder;
import org.geysermc.geyser.pack.option.OptionHolder;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.GeyserIntegratedPackUtil;
import java.util.AbstractMap;
import java.util.ArrayList;
@@ -55,7 +56,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent {
public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent implements GeyserIntegratedPackUtil {
/**
* The packs for this Session. A {@link ResourcePackHolder} may contain resource pack options registered
@@ -73,6 +74,8 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
private final GeyserSession session;
private boolean willCdnFail = false;
public SessionLoadResourcePacksEventImpl(GeyserSession session) {
super(session);
this.session = session;
@@ -88,6 +91,9 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
@Override
public boolean register(@NonNull ResourcePack resourcePack) {
try {
if (handlePossibleOptionalPack(resourcePack)) {
return false;
}
register(resourcePack, PriorityOption.NORMAL);
} catch (ResourcePackException e) {
GeyserImpl.getInstance().getLogger().error("An exception occurred while registering resource pack: " + e.getMessage(), e);
@@ -103,6 +109,10 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION);
}
if (handlePossibleOptionalPack(resourcePack)) {
return;
}
UUID uuid = resourcePack.uuid();
if (packs.containsKey(uuid)) {
throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE);
@@ -201,7 +211,14 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE
public List<ResourcePacksInfoPacket.Entry> infoPacketEntries() {
List<ResourcePacksInfoPacket.Entry> entries = new ArrayList<>();
boolean anyCdn = packs.values().stream().anyMatch(holder -> holder.codec() instanceof UrlPackCodec);
boolean warned = false;
for (ResourcePackHolder holder : packs.values()) {
if (!warned && anyCdn && !(holder.codec() instanceof UrlPackCodec)) {
GeyserImpl.getInstance().getLogger().warning("Mixing pack codecs will result in all UrlPackCodec delivered packs to fall back to non-cdn delivery!");
warned = true;
}
GeyserResourcePack pack = holder.pack();
ResourcePackManifest.Header header = pack.manifest().header();
entries.add(new ResourcePacksInfoPacket.Entry(

View File

@@ -236,15 +236,18 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
// Can happen if an error occurs in the resource pack event; that'll disconnect the player
return PacketSignal.HANDLED;
}
session.includedPackActive(resourcePackLoadEvent.isIntegratedPackActive());
ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries());
resourcePacksInfo.setVibrantVisualsForceDisabled(!session.isAllowVibrantVisuals());
resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().config().gameplay().forceResourcePacks() ||
GeyserImpl.getInstance().config().gameplay().enableOptionalPack());
resourcePackLoadEvent.isIntegratedPackActive());
resourcePacksInfo.setWorldTemplateId(UUID.randomUUID());
resourcePacksInfo.setWorldTemplateVersion("*");
GeyserImpl.getInstance().getLogger().info(resourcePacksInfo.toString());
session.sendUpstreamPacket(resourcePacksInfo);
GeyserLocale.loadGeyserLocale(session.locale());

View File

@@ -134,8 +134,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
// Load all remote resource packs from the config before firing the new event
// TODO configurate
//packMap.putAll(loadRemotePacks());
packMap.putAll(loadRemotePacks());
GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap);
GeyserImpl.getInstance().eventBus().fire(defineEvent);
@@ -239,8 +238,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<UUID, Resour
}
}
//List<String> remotePackUrls = instance.getConfig().getResourcePackUrls();
List<String> remotePackUrls = List.of();
List<String> remotePackUrls = instance.config().advanced().resourcePackUrls();
Map<UUID, ResourcePackHolder> packMap = new Object2ObjectOpenHashMap<>();
for (String url : remotePackUrls) {

View File

@@ -748,6 +748,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Accessors(fluent = true)
private boolean hasAcceptedCodeOfConduct = false;
@Accessors(fluent = true)
@Setter
private boolean includedPackActive = false;
private final Set<InputLocksFlag> inputLocksSet = EnumSet.noneOf(InputLocksFlag.class);
private boolean inputLockDirty;

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2025 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.util;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.pack.PackCodec;
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.api.pack.option.PriorityOption;
import org.geysermc.geyser.api.pack.option.ResourcePackOption;
import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl;
import org.geysermc.geyser.pack.ResourcePackHolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public interface GeyserIntegratedPackUtil {
GeyserImpl instance = GeyserImpl.getInstance();
UUID PACK_UUID = UUID.fromString("e5f5c938-a701-11eb-b2a3-047d7bb283ba");
Path CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
Path PACK_PATH = CACHE.resolve("GeyserIntegratedPack.mcpack");
AtomicBoolean PACK_ENABLED = new AtomicBoolean(false);
default void registerGeyserPack(GeyserDefineResourcePacksEventImpl event) {
if (!instance.config().gameplay().enableIntegratedPack()) {
return;
}
var pack = event.getPacks().get(PACK_UUID);
if (pack != null) {
if (pack.codec() instanceof UrlPackCodec) {
// Must ensure correct place in pack stack
pack.optionHolder().put(ResourcePackOption.Type.PRIORITY, PriorityOption.HIGH);
instance.getLogger().info("Not adding our own copy of the integrated pack due to url pack codec presence!");
PACK_ENABLED.set(true);
return;
}
warnOptionalPackPresent(warnMessageLocation(pack.codec()));
getPacks().remove(PACK_UUID);
}
try {
Files.createDirectories(CACHE);
Files.copy(GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("GeyserIntegratedPack.mcpack"),
PACK_PATH, StandardCopyOption.REPLACE_EXISTING);
event.register(ResourcePack.create(PathPackCodec.path(PACK_PATH)), PriorityOption.HIGH);
PACK_ENABLED.set(true);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Could not copy over Geyser integrated resource pack!", e);
}
}
default boolean handlePossibleOptionalPack(ResourcePack pack) {
if (!PACK_ENABLED.get()) {
return false;
}
if (!Objects.equals(pack.uuid(), PACK_UUID)) {
return false;
}
if (pack.codec() instanceof UrlPackCodec) {
getPacks().remove(PACK_UUID);
instance.getLogger().info("Overriding our own integrated pack with url pack codec delivered pack!");
return false;
}
warnOptionalPackPresent(warnMessageLocation(pack.codec()));
return true;
}
Map<UUID, ResourcePackHolder> getPacks();
default String warnMessageLocation(PackCodec codec) {
if (codec instanceof PathPackCodec pathPackCodec) {
return "(found in: %s)".formatted(instance.getBootstrap().getConfigFolder().relativize(pathPackCodec.path()));
}
return "(registered with codec: %s)".formatted(codec);
}
default void warnOptionalPackPresent(String message) {
instance.getLogger().warning("Detected duplicate GeyserOptionalPack registration! " +
" It should be removed " + message + ", as Geyser now includes an improved version of this resource pack by default!"
);
}
default boolean isIntegratedPackActive() {
return instance.config().gameplay().enableIntegratedPack() && getPacks().containsKey(PACK_UUID);
}
}

View File

@@ -109,14 +109,24 @@ public class WebUtils {
* @param fileLocation Location to save on disk
*/
public static void downloadFile(String reqURL, String fileLocation) {
downloadFile(reqURL, Paths.get(fileLocation));
}
/**
* Downloads a file from the given URL and saves it to disk
*
* @param reqURL File to fetch
* @param path Location to save on disk as a path
*/
public static void downloadFile(String reqURL, Path path) {
try {
HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection();
con.setRequestProperty("User-Agent", getUserAgent());
checkResponseCode(con);
InputStream in = con.getInputStream();
Files.copy(in, Paths.get(fileLocation), StandardCopyOption.REPLACE_EXISTING);
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Unable to download and save file: " + fileLocation + " (" + reqURL + ")", e);
throw new RuntimeException("Unable to download and save file: " + path.toAbsolutePath() + " (" + reqURL + ")", e);
}
}

Binary file not shown.