diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index 7094812a0f9..db37c921d51 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -33,7 +33,10 @@ import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; +import org.geysermc.geyser.api.preference.Preference; +import org.geysermc.geyser.api.preference.PreferenceKey; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -95,4 +98,12 @@ public interface GeyserConnection extends Connection, CommandSource { */ @NonNull Set fogEffects(); + + void storePreference(@NonNull PreferenceKey key, @NonNull Preference preference); + + @NonNull + Preference requirePreference(@NonNull PreferenceKey key) throws IllegalArgumentException; + + @NonNull + Optional> getPreference(@NonNull PreferenceKey key); } diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/BooleanPreference.java b/api/src/main/java/org/geysermc/geyser/api/preference/BooleanPreference.java new file mode 100644 index 00000000000..2e575fa271b --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/BooleanPreference.java @@ -0,0 +1,43 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public abstract class BooleanPreference extends Preference { + + public BooleanPreference(@NonNull Boolean initialValue) { + super(initialValue); + } + + @Override + public void onFormResponse(@NonNull Object response) { + if (response instanceof Boolean booleanValue) { + update(booleanValue); + } + throw new IllegalArgumentException(response + " is not a boolean"); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/FloatPreference.java b/api/src/main/java/org/geysermc/geyser/api/preference/FloatPreference.java new file mode 100644 index 00000000000..64bf22306a2 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/FloatPreference.java @@ -0,0 +1,43 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public abstract class FloatPreference extends Preference { + + public FloatPreference(@NonNull Float initialValue) { + super(initialValue); + } + + @Override + public void onFormResponse(@NonNull Object response) throws IllegalArgumentException { + if (response instanceof Float floatValue) { + update(floatValue); + } + throw new IllegalArgumentException(response + " is not a float"); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/IntegerPreference.java b/api/src/main/java/org/geysermc/geyser/api/preference/IntegerPreference.java new file mode 100644 index 00000000000..409b6e60316 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/IntegerPreference.java @@ -0,0 +1,43 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public abstract class IntegerPreference extends Preference { + + public IntegerPreference(@NonNull Integer initialValue) { + super(initialValue); + } + + @Override + public void onFormResponse(@NonNull Object response) throws IllegalArgumentException { + if (response instanceof Integer integerValue) { + update(integerValue); + } + throw new IllegalArgumentException(response + " is not an integer"); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/Preference.java b/api/src/main/java/org/geysermc/geyser/api/preference/Preference.java new file mode 100644 index 00000000000..10bb33d927d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/Preference.java @@ -0,0 +1,61 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.cumulus.component.Component; +import org.geysermc.geyser.api.connection.GeyserConnection; + +import java.util.Objects; + +public abstract class Preference { + + @NonNull + private T value; + + public Preference(@NonNull T initialValue) { + this.value = Objects.requireNonNull(initialValue, "initialValue"); + } + + @NonNull + public final T value() { + return value; + } + + public final void update(@NonNull T newValue) { + this.value = Objects.requireNonNull(newValue, "newValue"); + } + + public abstract boolean isModifiable(GeyserConnection connection); + + public abstract Component component(GeyserConnection connection); + + public abstract void onFormResponse(@NonNull Object response) throws IllegalArgumentException; + + public void onUpdate(GeyserConnection connection) { + // no-op by default + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/PreferenceKey.java b/api/src/main/java/org/geysermc/geyser/api/preference/PreferenceKey.java new file mode 100644 index 00000000000..a0063c7f6ee --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/PreferenceKey.java @@ -0,0 +1,29 @@ +/* + * 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.preference; + +public record PreferenceKey(String key) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/preference/StringPreference.java b/api/src/main/java/org/geysermc/geyser/api/preference/StringPreference.java new file mode 100644 index 00000000000..24f1d8e3eab --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/preference/StringPreference.java @@ -0,0 +1,43 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public abstract class StringPreference extends Preference { + + public StringPreference(@NonNull String initialValue) { + super(initialValue); + } + + @Override + public void onFormResponse(@NonNull Object response) throws IllegalArgumentException { + if (response instanceof String stringValue) { + update(stringValue); + } + throw new IllegalArgumentException(response + " is not a string"); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/CooldownType.java b/core/src/main/java/org/geysermc/geyser/configuration/CooldownType.java new file mode 100644 index 00000000000..123de4c1aed --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/CooldownType.java @@ -0,0 +1,74 @@ +/* + * 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.configuration; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.IOException; + +@Getter +@AllArgsConstructor +public enum CooldownType { + TITLE("options.attack.crosshair"), + ACTIONBAR("options.attack.hotbar"), + DISABLED("options.off"); + + public static final String OPTION_DESCRIPTION = "options.attackIndicator"; + public static final CooldownType[] VALUES = values(); + + private final String translation; + + /** + * Convert the CooldownType string (from config) to the enum, DISABLED on fail + * + * @param name CooldownType string + * @return The converted CooldownType + */ + public static CooldownType getByName(String name) { + if (name.equalsIgnoreCase("true")) { // Backwards config compatibility + return CooldownType.TITLE; + } + + for (CooldownType type : VALUES) { + if (type.name().equalsIgnoreCase(name)) { + return type; + } + } + return DISABLED; + } + + public static class Deserializer extends JsonDeserializer { + @Override + public CooldownType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + return CooldownType.getByName(p.getValueAsString()); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index 222af341b35..133fb870175 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -82,7 +82,7 @@ public interface GeyserConfiguration { boolean isAllowThirdPartyEars(); - String getShowCooldown(); + CooldownType getShowCooldown(); boolean isShowCoordinates(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index e096d58fa68..8c0eb692db9 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -99,8 +99,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("allow-third-party-capes") private boolean allowThirdPartyCapes = true; + @JsonDeserialize(using = CooldownType.Deserializer.class) @JsonProperty("show-cooldown") - private String showCooldown = "title"; + private CooldownType showCooldown = CooldownType.TITLE; @JsonProperty("show-coordinates") private boolean showCoordinates = true; diff --git a/core/src/main/java/org/geysermc/geyser/preference/CooldownPreference.java b/core/src/main/java/org/geysermc/geyser/preference/CooldownPreference.java new file mode 100644 index 00000000000..4087f15b36e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/preference/CooldownPreference.java @@ -0,0 +1,70 @@ +/* + * 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.preference; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.cumulus.component.Component; +import org.geysermc.cumulus.component.DropdownComponent; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.preference.Preference; +import org.geysermc.geyser.api.preference.PreferenceKey; +import org.geysermc.geyser.configuration.CooldownType; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.Arrays; +import java.util.List; + +public class CooldownPreference extends Preference { + + public static final PreferenceKey KEY = new PreferenceKey<>("geyser:cooldown_type"); + + private static final List OPTIONS = Arrays.stream(CooldownType.VALUES) + .map(CooldownType::getTranslation) + .toList(); + + public CooldownPreference(GeyserSession session) { + super(configSetting(session)); + } + + @Override + public boolean isModifiable(GeyserConnection connection) { + return configSetting((GeyserSession) connection) != CooldownType.DISABLED; + } + + @Override + public Component component(GeyserConnection connection) { + return DropdownComponent.of(CooldownType.OPTION_DESCRIPTION, OPTIONS, value().ordinal()); + } + + @Override + public void onFormResponse(@NonNull Object response) throws IllegalArgumentException { + update(CooldownType.VALUES[(int) response]); + } + + private static CooldownType configSetting(GeyserSession session) { + return session.getGeyser().getConfig().getShowCooldown(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/preference/CustomSkullsPreference.java b/core/src/main/java/org/geysermc/geyser/preference/CustomSkullsPreference.java new file mode 100644 index 00000000000..fdca24fd087 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/preference/CustomSkullsPreference.java @@ -0,0 +1,56 @@ +/* + * 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.preference; + +import org.geysermc.cumulus.component.Component; +import org.geysermc.cumulus.component.ToggleComponent; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.preference.BooleanPreference; +import org.geysermc.geyser.api.preference.PreferenceKey; +import org.geysermc.geyser.session.GeyserSession; + +public class CustomSkullsPreference extends BooleanPreference { + + public static final PreferenceKey KEY = new PreferenceKey<>("geyser:show_custom_skulls"); + + public CustomSkullsPreference(GeyserSession session) { + super(configSetting(session)); + } + + @Override + public boolean isModifiable(GeyserConnection connection) { + return configSetting((GeyserSession) connection); + } + + @Override + public Component component(GeyserConnection connection) { + return ToggleComponent.of("geyser.settings.option.customSkulls", value()); + } + + private static boolean configSetting(GeyserSession session) { + return session.getGeyser().getConfig().isAllowCustomSkulls(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/preference/ShowCoordinatesPreference.java b/core/src/main/java/org/geysermc/geyser/preference/ShowCoordinatesPreference.java new file mode 100644 index 00000000000..f760436db78 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/preference/ShowCoordinatesPreference.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.preference; + +import org.geysermc.cumulus.component.Component; +import org.geysermc.cumulus.component.ToggleComponent; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.preference.BooleanPreference; +import org.geysermc.geyser.api.preference.PreferenceKey; +import org.geysermc.geyser.session.GeyserSession; + +public class ShowCoordinatesPreference extends BooleanPreference { + + public static final PreferenceKey KEY = new PreferenceKey<>("geyser:show_coordinates"); + + public ShowCoordinatesPreference(GeyserSession session) { + super(isAllowed(session)); + } + + @Override + public boolean isModifiable(GeyserConnection connection) { + return isAllowed((GeyserSession) connection); + } + + @Override + public Component component(GeyserConnection connection) { + return ToggleComponent.of("%createWorldScreen.showCoordinates", value()); + } + + @Override + public void onUpdate(GeyserConnection connection) { + boolean showCoordinates; + if (isModifiable(connection)) { + showCoordinates = value(); + } else { + showCoordinates = false; + } + + GeyserSession session = (GeyserSession) connection; + session.sendGameRule("showcoordinates", showCoordinates); + } + + private static boolean isAllowed(GeyserSession session) { + return !session.isReducedDebugInfo() && session.getGeyser().getConfig().isShowCoordinates(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index dba4bd112e3..3ec677eaff9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -99,6 +99,8 @@ import org.geysermc.api.util.InputMode; import org.geysermc.api.util.UiProfile; import org.geysermc.geyser.api.bedrock.camera.CameraShake; +import org.geysermc.geyser.api.preference.Preference; +import org.geysermc.geyser.api.preference.PreferenceKey; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.util.FormBuilder; @@ -1991,6 +1993,21 @@ public void removeFog(String... fogNameSpaces) { return Set.copyOf(this.appliedFog); } + @Override + public void storePreference(@NonNull PreferenceKey key, @NonNull Preference preference) { + this.preferencesCache.register(key, preference); + } + + @Override + public @NonNull Preference requirePreference(@NonNull PreferenceKey key) throws IllegalArgumentException { + return this.preferencesCache.require(key); + } + + @Override + public @NonNull Optional> getPreference(@NonNull PreferenceKey key) { + return this.preferencesCache.get(key); + } + public void addCommandEnum(String name, String enums) { softEnumPacket(name, SoftEnumUpdateType.ADD, enums); } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java index 9e8597b0f54..d009a5d47f8 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java @@ -25,63 +25,84 @@ package org.geysermc.geyser.session.cache; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; -import lombok.Setter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.preference.Preference; +import org.geysermc.geyser.api.preference.PreferenceKey; +import org.geysermc.geyser.configuration.CooldownType; import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.preference.CooldownPreference; +import org.geysermc.geyser.preference.CustomSkullsPreference; +import org.geysermc.geyser.preference.ShowCoordinatesPreference; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.util.CooldownUtils; -@Getter +import java.util.Map; +import java.util.Optional; + public class PreferencesCache { private final GeyserSession session; - /** - * True if the client prefers being shown their coordinates, regardless if they're being shown or not. - * This will be true everytime the client joins the server because neither the client nor server store the preference permanently. - */ - @Setter - private boolean prefersShowCoordinates = true; + @Getter + private final Map, Preference> preferences = new Object2ObjectOpenHashMap<>(); - /** - * If the client's preference will be ignored, this will return false. - */ - private boolean allowShowCoordinates; + public PreferencesCache(GeyserSession session) { + this.session = session; - /** - * If the session wants custom skulls to be shown. - */ - @Setter - private boolean prefersCustomSkulls; + register(CooldownPreference.KEY, new CooldownPreference(session)); + register(CustomSkullsPreference.KEY, new CustomSkullsPreference(session)); + register(ShowCoordinatesPreference.KEY, new ShowCoordinatesPreference(session)); + } - /** - * Which CooldownType the client prefers. Initially set to {@link CooldownUtils#getDefaultShowCooldown()}. - */ - @Setter - private CooldownUtils.CooldownType cooldownPreference = CooldownUtils.getDefaultShowCooldown(); + public void register(PreferenceKey key, Preference preference) { + if (preference == null) { + throw new IllegalArgumentException("preference cannot be null"); + } + preferences.put(key, preference); + } - public PreferencesCache(GeyserSession session) { - this.session = session; + @SuppressWarnings("unchecked") + @NonNull + public Preference require(PreferenceKey key) throws IllegalArgumentException { + Preference preference = (Preference) preferences.get(key); + if (preference == null) { + throw new IllegalArgumentException("preference with key " + key + " is not stored for session " + session.javaUuid()); + } + return preference; + } - prefersCustomSkulls = session.getGeyser().getConfig().isAllowCustomSkulls(); + @SuppressWarnings("unchecked") + @NonNull + public Optional> get(PreferenceKey key) { + return Optional.ofNullable((Preference) preferences.get(key)); } /** - * Tell the client to hide or show the coordinates. - * - * If {@link #prefersShowCoordinates} is true, coordinates will be shown, unless either of the following conditions apply:
- *
- * {@link GeyserSession#reducedDebugInfo} is enabled - * {@link GeyserConfiguration#isShowCoordinates()} is disabled + * Tell the client to hide or show the coordinates. The client's preference will be overridden if either of the + * following are true: + *

+ * {@link GeyserSession#isReducedDebugInfo} is enabled.
+ * {@link GeyserConfiguration#isShowCoordinates()} is disabled. */ public void updateShowCoordinates() { - allowShowCoordinates = !session.isReducedDebugInfo() && session.getGeyser().getConfig().isShowCoordinates(); - session.sendGameRule("showcoordinates", allowShowCoordinates && prefersShowCoordinates); + Preference preference = require(ShowCoordinatesPreference.KEY); + // preference itself won't be any different, but trigger an update anyway in case + // reduced-debug-info has changed or the config has changed + preference.onUpdate(session); } - /** - * @return true if the session prefers custom skulls, and the config allows them. - */ - public boolean showCustomSkulls() { - return prefersCustomSkulls && session.getGeyser().getConfig().isAllowCustomSkulls(); + public boolean getEffectiveShowSkulls() { + if (!session.getGeyser().getConfig().isAllowCustomSkulls()) { + return false; + } + return require(CustomSkullsPreference.KEY).value(); + } + + + public CooldownType getEffectiveCooldown() { + if (session.getGeyser().getConfig().getShowCooldown() == CooldownType.DISABLED) { + return CooldownType.DISABLED; + } + return require(CooldownPreference.KEY).value(); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java index e1a94f02caa..ac735ee07da 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java @@ -63,7 +63,7 @@ public void translate(GeyserSession session, ClientboundBlockEntityDataPacket pa BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(type, position.getX(), position.getY(), position.getZ(), packet.getNbt(), blockState), packet.getPosition()); // Check for custom skulls. - if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt() != null && packet.getNbt().contains("SkullOwner")) { + if (session.getPreferencesCache().getEffectiveShowSkulls() && packet.getNbt() != null && packet.getNbt().contains("SkullOwner")) { SkullBlockEntityTranslator.translateSkull(session, packet.getNbt(), position.getX(), position.getY(), position.getZ(), blockState); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java index a6d5fe09c9a..e1b44c9a83d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java @@ -90,6 +90,7 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke int yOffset = session.getChunkCache().getChunkMinY(); int chunkSize = session.getChunkCache().getChunkHeightY(); int biomeGlobalPalette = session.getBiomeGlobalPalette(); + boolean customSkulls = session.getPreferencesCache().getEffectiveShowSkulls(); DataPalette[] javaChunks = new DataPalette[chunkSize]; DataPalette[] javaBiomes = new DataPalette[chunkSize]; @@ -283,7 +284,7 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState)); // Check for custom skulls - if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) { + if (customSkulls && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) { SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java index c00e389fd21..b7346382b7c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java @@ -26,7 +26,7 @@ package org.geysermc.geyser.util; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; -import lombok.Getter; +import org.geysermc.geyser.configuration.CooldownType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PreferencesCache; import org.geysermc.geyser.text.ChatColor; @@ -40,8 +40,8 @@ public class CooldownUtils { private static CooldownType DEFAULT_SHOW_COOLDOWN; - public static void setDefaultShowCooldown(String showCooldown) { - DEFAULT_SHOW_COOLDOWN = CooldownType.getByName(showCooldown); + public static void setDefaultShowCooldown(CooldownType showCooldown) { + DEFAULT_SHOW_COOLDOWN = showCooldown; } public static CooldownType getDefaultShowCooldown() { @@ -54,7 +54,7 @@ public static CooldownType getDefaultShowCooldown() { */ public static void sendCooldown(GeyserSession session) { if (DEFAULT_SHOW_COOLDOWN == CooldownType.DISABLED) return; - CooldownType sessionPreference = session.getPreferencesCache().getCooldownPreference(); + CooldownType sessionPreference = session.getPreferencesCache().getEffectiveCooldown(); if (sessionPreference == CooldownType.DISABLED) return; if (session.getAttackSpeed() == 0.0 || session.getAttackSpeed() > 20) return; // 0.0 usually happens on login and causes issues with visuals; anything above 20 means a plugin like OldCombatMechanics is being used @@ -145,32 +145,4 @@ private static String getTitle(GeyserSession session) { return builder.toString(); } - @Getter - public enum CooldownType { - TITLE, - ACTIONBAR, - DISABLED; - - public static final CooldownType[] VALUES = values(); - - /** - * Convert the CooldownType string (from config) to the enum, DISABLED on fail - * - * @param name CooldownType string - * - * @return The converted CooldownType - */ - public static CooldownType getByName(String name) { - if (name.equalsIgnoreCase("true")) { // Backwards config compatibility - return CooldownType.TITLE; - } - - for (CooldownType type : VALUES) { - if (type.name().equalsIgnoreCase(name)) { - return type; - } - } - return DISABLED; - } - } } diff --git a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java index 5957fb9d98f..1b810da83cc 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java @@ -27,7 +27,9 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; +import it.unimi.dsi.fastutil.Pair; import org.geysermc.cumulus.component.DropdownComponent; +import org.geysermc.cumulus.component.LabelComponent; import org.geysermc.cumulus.form.CustomForm; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.level.GameRule; @@ -36,6 +38,8 @@ import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; +import java.util.Objects; + public class SettingsUtils { /** * Build a settings form for the given session and store it for later @@ -51,29 +55,21 @@ public static CustomForm buildForm(GeyserSession session) { .title("geyser.settings.title.main") .iconPath("textures/ui/settings_glyph_color_2x.png"); - // Only show the client title if any of the client settings are available - boolean showClientSettings = session.getPreferencesCache().isAllowShowCoordinates() - || CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED - || session.getGeyser().getConfig().isAllowCustomSkulls(); + var preferences = session.getPreferencesCache().getPreferences().values() + .stream() + .filter(pref -> pref.isModifiable(session)) // only show modifiable preferences + .map(pref -> Pair.of(pref, pref.component(session))) // compute components + .filter(p -> !(p.value() instanceof LabelComponent)) // skip any that gave us a label + .toList(); + + // Only show the client title if any of the client settings are available + boolean showClientSettings = !preferences.isEmpty(); if (showClientSettings) { builder.label("geyser.settings.title.client"); - // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. - if (session.getPreferencesCache().isAllowShowCoordinates()) { - builder.toggle("%createWorldScreen.showCoordinates", session.getPreferencesCache().isPrefersShowCoordinates()); - } - - if (CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - DropdownComponent.Builder cooldownDropdown = DropdownComponent.builder("options.attackIndicator"); - cooldownDropdown.option("options.attack.crosshair", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.TITLE); - cooldownDropdown.option("options.attack.hotbar", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.ACTIONBAR); - cooldownDropdown.option("options.off", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.DISABLED); - builder.dropdown(cooldownDropdown); - } - - if (session.getGeyser().getConfig().isAllowCustomSkulls()) { - builder.toggle("geyser.settings.option.customSkulls", session.getPreferencesCache().isPrefersCustomSkulls()); + for (var preferenceData : preferences) { + builder.component(preferenceData.value()); } } @@ -112,19 +108,9 @@ public static CustomForm buildForm(GeyserSession session) { builder.validResultHandler((response) -> { if (showClientSettings) { - // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. - if (session.getPreferencesCache().isAllowShowCoordinates()) { - session.getPreferencesCache().setPrefersShowCoordinates(response.next()); - session.getPreferencesCache().updateShowCoordinates(); - } - - if (CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - CooldownUtils.CooldownType cooldownType = CooldownUtils.CooldownType.VALUES[(int) response.next()]; - session.getPreferencesCache().setCooldownPreference(cooldownType); - } - - if (session.getGeyser().getConfig().isAllowCustomSkulls()) { - session.getPreferencesCache().setPrefersCustomSkulls(response.next()); + for (var preferenceData : preferences) { + Object value = Objects.requireNonNull(response.next(), "response for preference " + preferenceData.key()); + preferenceData.key().onFormResponse(value); } }