Skip to content

Commit 3c6ced4

Browse files
Add support for bedrock's dyeable component in the custom item API
1 parent 9b42045 commit 3c6ced4

5 files changed

Lines changed: 92 additions & 4 deletions

File tree

api/src/main/java/org/geysermc/geyser/api/item/custom/v2/CustomItemBedrockOptions.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525

2626
package org.geysermc.geyser.api.item.custom.v2;
2727

28+
import org.checkerframework.checker.index.qual.NonNegative;
2829
import org.checkerframework.common.returnsreceiver.qual.This;
30+
import org.checkerframework.common.value.qual.IntRange;
2931
import org.geysermc.geyser.api.GeyserApi;
3032
import org.geysermc.geyser.api.util.CreativeCategory;
3133
import org.geysermc.geyser.api.util.Identifier;
3234
import org.jspecify.annotations.Nullable;
3335

36+
import java.util.OptionalInt;
3437
import java.util.Set;
3538

3639
/**
@@ -95,13 +98,23 @@ public interface CustomItemBedrockOptions {
9598

9699
/**
97100
* Gets the item's set of bedrock tags that can be used in Molang.
98-
* Equivalent to "tag:some_tag"
101+
* Equivalent to tag:some_tag".
99102
*
100103
* @return the item's set of bedrock tags, can be empty
101104
* @since 2.9.3
102105
*/
103106
Set<Identifier> tags();
104107

108+
/**
109+
* Returns the default colour of the item if it is dyeable, and if not, an empty optional.
110+
*
111+
* <p>The client allows dyeing dyeable items using any of the vanilla dyes in a crafting table.</p>
112+
*
113+
* @return the default colour of the item if it is dyeable, and if not, an empty optional
114+
* @since 2.10.1
115+
*/
116+
OptionalInt dyeable();
117+
105118
/**
106119
* Creates a new builder for custom item bedrock options.
107120
*
@@ -160,7 +173,7 @@ interface Builder {
160173
* @since 2.9.3
161174
*/
162175
@This
163-
Builder protectionValue(int protectionValue);
176+
Builder protectionValue(@NonNegative int protectionValue);
164177

165178
/**
166179
* Sets the item's creative category.
@@ -199,12 +212,46 @@ interface Builder {
199212
* Sets the item's set of bedrock tags, for use in Molang. Pass {@code null} to clear all tags.
200213
*
201214
* @param tags the tags to be set, or {@code null} to clear all tags
215+
* @see CustomItemBedrockOptions#tags()
202216
* @return this builder
203217
* @since 2.9.3
204218
*/
205219
@This
206220
Builder tags(@Nullable Set<Identifier> tags);
207221

222+
/**
223+
* Marks the item as dyeable, and sets the default color of the item.
224+
*
225+
* @param defaultColor the default color of the item
226+
* @see CustomItemBedrockOptions#dyeable()
227+
* @return this builder
228+
* @since 2.10.1
229+
*/
230+
@This
231+
Builder dyeable(@IntRange(from = 0x000000, to = 0xFFFFFF) int defaultColor);
232+
233+
/**
234+
* Marks the item as dyeable, and sets the default color of the item.
235+
*
236+
* @param defaultRed the red component of the default color of the item
237+
* @param defaultGreen the green component of the default color of the item
238+
* @param defaultBlue the blue component of the default color of the item
239+
* @see CustomItemBedrockOptions#dyeable()
240+
* @return this builder
241+
* @since 2.10.1
242+
*/
243+
@This
244+
default Builder dyeable(@IntRange(from = 0, to = 255) int defaultRed, @IntRange(from = 0, to = 255) int defaultGreen, @IntRange(from = 0, to = 255) int defaultBlue) {
245+
if (defaultRed < 0 || defaultRed > 255) {
246+
throw new IllegalArgumentException("Red component must be between 0 and 255, was: " + defaultRed);
247+
}else if (defaultGreen < 0 || defaultGreen > 255) {
248+
throw new IllegalArgumentException("Green component must be between 0 and 255, was: " + defaultGreen);
249+
}else if (defaultBlue < 0 || defaultBlue > 255) {
250+
throw new IllegalArgumentException("Blue component must be between 0 and 255, was: " + defaultBlue);
251+
}
252+
return dyeable((defaultRed << 16) | (defaultGreen << 8) | defaultBlue);
253+
}
254+
208255
/**
209256
* Creates the custom item bedrock options.
210257
*

core/src/main/java/org/geysermc/geyser/item/custom/GeyserCustomItemBedrockOptions.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@
2727

2828
import org.checkerframework.checker.nullness.qual.NonNull;
2929
import org.checkerframework.checker.nullness.qual.Nullable;
30+
import org.checkerframework.common.returnsreceiver.qual.This;
3031
import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions;
3132
import org.geysermc.geyser.api.util.CreativeCategory;
3233
import org.geysermc.geyser.api.util.Identifier;
3334
import org.geysermc.geyser.registry.populator.custom.CustomItemContext;
3435
import org.jetbrains.annotations.NotNull;
3536

3637
import java.util.HashSet;
38+
import java.util.Locale;
3739
import java.util.Objects;
40+
import java.util.OptionalInt;
3841
import java.util.Set;
3942

4043
public record GeyserCustomItemBedrockOptions(@Nullable String icon, boolean allowOffhand, boolean displayHandheld, int protectionValue,
41-
@NonNull CreativeCategory creativeCategory, @Nullable String creativeGroup, @NonNull Set<Identifier> tags) implements CustomItemBedrockOptions {
44+
@NonNull CreativeCategory creativeCategory, @Nullable String creativeGroup, @NonNull Set<Identifier> tags,
45+
OptionalInt dyeable) implements CustomItemBedrockOptions {
4246

4347
@Override
4448
public int protectionValue() {
@@ -60,6 +64,7 @@ public static class Builder implements CustomItemBedrockOptions.Builder {
6064
private CreativeCategory creativeCategory = CreativeCategory.NONE;
6165
private String creativeGroup = null;
6266
private Set<Identifier> tags = new HashSet<>();
67+
private @Nullable Integer dyeableColor;
6368

6469
@Override
6570
public Builder icon(@Nullable String icon) {
@@ -81,6 +86,9 @@ public Builder displayHandheld(boolean displayHandheld) {
8186

8287
@Override
8388
public Builder protectionValue(int protectionValue) {
89+
if (protectionValue < 0) {
90+
throw new IllegalArgumentException("protectionValue cannot be negative");
91+
}
8492
this.protectionValue = protectionValue;
8593
return this;
8694
}
@@ -111,10 +119,19 @@ public Builder tags(@Nullable Set<Identifier> tags) {
111119
return this;
112120
}
113121

122+
@Override
123+
public CustomItemBedrockOptions.@This Builder dyeable(int defaultColor) {
124+
if (defaultColor < 0 || defaultColor > 0xFFFFFF) {
125+
throw new IllegalArgumentException("defaultColor must be between 0x000000 and 0xFFFFFF, was: 0x" + Integer.toString(defaultColor, 16).toUpperCase(Locale.ROOT));
126+
}
127+
this.dyeableColor = defaultColor;
128+
return this;
129+
}
130+
114131
@Override
115132
public CustomItemBedrockOptions build() {
116133
return new GeyserCustomItemBedrockOptions(icon, allowOffhand, displayHandheld, protectionValue,
117-
creativeCategory, creativeGroup, Set.copyOf(tags));
134+
creativeCategory, creativeGroup, Set.copyOf(tags), dyeableColor == null ? OptionalInt.empty() : OptionalInt.of(dyeableColor));
118135
}
119136
}
120137
}

core/src/main/java/org/geysermc/geyser/registry/mappings/definition/SingleDefinitionReader.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ private static void readBedrockOptions(CustomItemDefinition.Builder definitionBu
147147
MappingsUtil.readIfPresent(bedrockOptions, "creative_category", builder::creativeCategory, NodeReader.CREATIVE_CATEGORY, context);
148148
MappingsUtil.readIfPresent(bedrockOptions, "creative_group", builder::creativeGroup, NodeReader.NON_EMPTY_STRING, context);
149149
MappingsUtil.readArrayIfPresent(bedrockOptions, "tags", tags -> builder.tags(new HashSet<>(tags)), NodeReader.IDENTIFIER, context);
150+
MappingsUtil.readIfPresent(bedrockOptions, "dyeable", builder::dyeable, NodeReader.HEX_INT, context);
150151

151152
definitionBuilder.bedrockOptions(builder);
152153
}

core/src/main/java/org/geysermc/geyser/registry/mappings/util/NodeReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ public interface NodeReader<T> {
5959

6060
NodeReader<Integer> POSITIVE_INT = INT.validate(i -> i > 0, "integer must be positive");
6161

62+
NodeReader<Integer> HEX_INT = node -> {
63+
String s = node.getAsString();
64+
if (s.startsWith("#")) {
65+
s = s.substring(1);
66+
}
67+
try {
68+
return Integer.parseInt(s, 16);
69+
} catch (NumberFormatException exception) {
70+
throw new InvalidCustomMappingsFileException("failed to parse hexadecimal string: " + s);
71+
}
72+
};
73+
6274
NodeReader<Double> DOUBLE = JsonPrimitive::getAsDouble;
6375

6476
NodeReader<Double> NON_NEGATIVE_DOUBLE = DOUBLE.validate(d -> d >= 0, "number must be non-negative");

core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@ private static NbtMapBuilder createComponentNbt(Key itemIdentifier, CustomItemCo
296296
.build());
297297
}
298298

299+
context.definition().bedrockOptions().dyeable().ifPresent(defaultColor -> {
300+
// Please note that we are not including this component by default for items that override vanilla dyeable items, like leather armour
301+
// This is because on Java, dyeable items are defined through recipes. As such, we don't know at this time whether the vanilla item actually is a
302+
// dyeable: it could've been removed from the recipe!
303+
// This unfortunately also applies to vanilla items that have been made dyeable through a nice recipe: again, we don't know about this recipe's existance here.
304+
// As such, we have to rely on users putting this component in their mappings.
305+
componentBuilder.putCompound("minecraft:dyeable", NbtMap.builder()
306+
.putIntArray("default_color", new int[]{(defaultColor & 0xFF0000) >> 16, (defaultColor & 0x00FF00) >> 8, defaultColor & 0xFF})
307+
.build());
308+
});
309+
299310
AttackRange attackRange = context.components().getOrDefault(DataComponentTypes.ATTACK_RANGE, DEFAULT_ATTACK_RANGE);
300311

301312
KineticWeapon kineticWeapon = context.components().get(DataComponentTypes.KINETIC_WEAPON);

0 commit comments

Comments
 (0)