composite rewriter

This commit is contained in:
Lulu13022002 2024-02-21 21:04:41 +01:00
parent d0e85d00f6
commit 6a881d69cd
No known key found for this signature in database
GPG Key ID: 491C8F0B8ACDEB01
14 changed files with 258 additions and 135 deletions

View File

@ -172,11 +172,14 @@ public interface Boat extends Vehicle {
public enum Status {
NOT_IN_WORLD, // Paper
// Paper start - Generated/BoatStatus
// @GeneratedFrom 1.20.4
IN_WATER,
UNDER_WATER,
UNDER_FLOWING_WATER,
ON_LAND,
IN_AIR;
// Paper end - Generated/BoatStatus
}
// Paper start

View File

@ -1,5 +1,6 @@
package io.papermc.generator;
import io.papermc.generator.rewriter.CompositeRewriter;
import io.papermc.generator.rewriter.SourceRewriter;
import io.papermc.generator.rewriter.types.EnumCloneRewriter;
import io.papermc.generator.rewriter.types.EnumRegistryRewriter;
@ -157,12 +158,15 @@ public interface Generators {
return "%s.%s".formatted(NamedTextColor.class.getCanonicalName(), rarity.color().name());
}
},
new EnumCloneRewriter<>(Boat.Type.class, net.minecraft.world.entity.vehicle.Boat.Type.class, "BoatType", false) {
@Override
protected String rewriteEnumValue(net.minecraft.world.entity.vehicle.Boat.Type type) {
return "%s.%s".formatted(Material.class.getSimpleName(), BuiltInRegistries.BLOCK.getKey(type.getPlanks()).getPath().toUpperCase(Locale.ENGLISH));
}
},
CompositeRewriter.bind(
new EnumCloneRewriter<>(Boat.Type.class, net.minecraft.world.entity.vehicle.Boat.Type.class, "BoatType", false) {
@Override
protected String rewriteEnumValue(net.minecraft.world.entity.vehicle.Boat.Type type) {
return "%s.%s".formatted(Material.class.getSimpleName(), BuiltInRegistries.BLOCK.getKey(type.getPlanks()).getPath().toUpperCase(Locale.ENGLISH));
}
},
new EnumCloneRewriter<>(Boat.Status.class, net.minecraft.world.entity.vehicle.Boat.Status.class, "BoatStatus", false)
),
new RegistryFieldRewriter<>(Structure.class, Registries.STRUCTURE, "Structure", "getStructure"),
new RegistryFieldRewriter<>(StructureType.class, Registries.STRUCTURE_TYPE, "StructureType", "getStructureType"),
new RegistryFieldRewriter<>(TrimPattern.class, Registries.TRIM_PATTERN, "TrimPattern", null),

View File

@ -0,0 +1,51 @@
package io.papermc.generator.rewriter;
import com.google.common.base.Preconditions;
import io.papermc.generator.rewriter.utils.ClassHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CompositeRewriter extends SearchReplaceRewriter {
private final Map<String, SearchReplaceRewriter> patternsInfo;
private CompositeRewriter(Class<?> rewriteClass, List<SearchReplaceRewriter> rewriters) {
super(rewriteClass, null, false);
this.patternsInfo = rewriters.stream().collect(Collectors.toMap(rewriter -> rewriter.pattern, rewriter -> rewriter));
}
@Override
protected void beginSearch() {
for (SearchReplaceRewriter rewriter : this.patternsInfo.values()) {
rewriter.beginSearch();
}
}
public static CompositeRewriter bind(SearchReplaceRewriter... rewriters) {
return bind(Arrays.asList(rewriters));
}
public static CompositeRewriter bind(List<SearchReplaceRewriter> rewriters) {
Preconditions.checkArgument(!rewriters.isEmpty(), "Rewriter list cannot be empty!");
Class<?> rewriteClass = rewriters.get(0).rewriteClass;
Class<?> rootClass = ClassHelper.getRootClass(rewriteClass);
for (SearchReplaceRewriter rewriter : rewriters) {
Preconditions.checkArgument(rewriter.pattern != null, "Rewriter pattern cannot be null!");
Preconditions.checkState(rewriteClass.getPackageName().equals(rewriter.rewriteClass.getPackageName()) &&
rootClass == ClassHelper.getRootClass(rewriter.rewriteClass), "Composite rewriter only works for one file!");
}
return new CompositeRewriter(rewriteClass, rewriters);
}
@Override
protected void searchAndReplace(BufferedReader reader, StringBuilder content, Map<String, SearchReplaceRewriter> patternInfo) throws IOException {
Preconditions.checkState(patternInfo.isEmpty());
super.searchAndReplace(reader, content, this.patternsInfo);
}
}

View File

@ -1,154 +1,190 @@
package io.papermc.generator.rewriter;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import io.papermc.generator.Main;
import io.papermc.generator.rewriter.utils.Annotations;
import io.papermc.generator.rewriter.utils.ClassHelper;
import io.papermc.generator.rewriter.utils.ImportCollector;
import io.papermc.generator.utils.Formatting;
import io.papermc.paper.generated.GeneratedFrom;
import net.minecraft.SharedConstants;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.ApiStatus;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class SearchReplaceRewriter implements SourceRewriter {
private static final String INDENT_UNIT = " ";
private static final String PAPER_START_FORMAT = "Paper start";
private static final String PAPER_END_FORMAT = "Paper end";
private static final String GENERATED_COMMENT_FORMAT = "// %s - Generated/%s"; // {0} = PAPER_START_FORMAT|PAPER_END_FORMAT {1} = pattern
protected final Class<?> rewriteClass;
private final String pattern;
protected final String pattern;
private final boolean equalsSize;
private final ImportCollector importCollector;
public SearchReplaceRewriter(Class<?> rewriteClass, String pattern, boolean equalsSize) {
this.rewriteClass = rewriteClass;
this.pattern = pattern;
this.equalsSize = equalsSize;
this.importCollector = new ImportCollector(rewriteClass);
}
// only when equalsSize = false
@ApiStatus.OverrideOnly
protected void insert(SearchMetadata metadata, StringBuilder builder) {}
// only when equalsSize = true
@ApiStatus.OverrideOnly
protected void replaceLine(SearchMetadata metadata, StringBuilder builder) {}
protected void beginSearch() {}
private boolean framed;
private SearchReplaceRewriter foundRewriter;
private StringBuilder searchAndReplace(List<String> lines, StringBuilder content) {
private void searchAndReplace(BufferedReader reader, StringBuilder content) throws IOException {
searchAndReplace(reader, content, this.pattern == null ? new HashMap<>() : Map.of(this.pattern, this));
}
protected void searchAndReplace(BufferedReader reader, StringBuilder content, Map<String, SearchReplaceRewriter> patternInfo) throws IOException {
Preconditions.checkState(!patternInfo.isEmpty());
this.beginSearch();
String indent = Formatting.incrementalIndent(INDENT_UNIT, this.rewriteClass);
String startPattern = String.format("// %s - Generated/%s", PAPER_START_FORMAT, this.pattern);
String endPattern = String.format("// %s - Generated/%s", PAPER_END_FORMAT, this.pattern);
Set<String> patterns = patternInfo.keySet();
Set<String> foundPatterns = new HashSet<>();
StringBuilder strippedContent = null;
StringBuilder strippedContent = new StringBuilder();
Class<?> rootClass = ClassHelper.getRootClass(this.rewriteClass);
ImportCollector importCollector = new ImportCollector(rootClass);
// todo support multiple passes
// strip the replaced content first or apply directly the change when the replaced content size is equals to the new content size
{
boolean replace = false;
String indent = Formatting.incrementalIndent(INDENT_UNIT, rootClass);
String rootClassDeclaration = "%s %s".formatted(ClassHelper.getDeclaredType(rootClass), rootClass.getSimpleName());
boolean inBody = false;
int i = 0;
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
// collect import to avoid fqn when not needed
// collect import to avoid fqn when not needed
if (!inBody) {
if (line.startsWith("import ") && line.endsWith(";")) {
this.importCollector.consume(line);
importCollector.consume(line);
}
if (line.contains(rootClassDeclaration)) { // might fail on comments but good enough
inBody = true;
}
}
if (line.equals(indent + endPattern)) {
if (this.framed) {
this.framed = false;
if (this.equalsSize) {
replace = false;
}
} else {
throw new IllegalStateException("Start generated comment missing for " + this.rewriteClass.getSimpleName() + " at line " + (i + 1));
Optional<String> endPattern = this.searchPattern(line, indent, PAPER_END_FORMAT, patterns);
if (endPattern.isPresent()) {
if (this.foundRewriter == null) {
throw new IllegalStateException("Start generated comment missing for pattern " + endPattern.get() + " in class " + this.rewriteClass.getSimpleName() + " at line " + (i + 1));
}
if (this.foundRewriter.pattern.equals(endPattern.get())) {
if (!this.foundRewriter.equalsSize) {
appendGeneratedComment(content, indent);
this.foundRewriter.insert(new SearchMetadata(importCollector, indent, strippedContent.toString(), i), content);
strippedContent = null;
}
this.foundRewriter = null;
} else {
throw new IllegalStateException("End generated comment doesn't match for pattern " + this.foundRewriter.pattern + " in " + this.rewriteClass.getSimpleName() + " at line " + (i + 1));
}
}
if (!this.framed) {
content.append(line);
content.append('\n');
if (this.foundRewriter == null) {
content.append(line);
content.append('\n');
} else {
if (this.foundRewriter.equalsSize) {
// there's no generated comment here since when the size is equals the replaced content doesn't depend on the game content
// if it does that means the replaced content might not be equals during MC update because of adding/removed content
this.foundRewriter.replaceLine(new SearchMetadata(importCollector, indent, line, i), content);
} else {
strippedContent.append(line);
strippedContent.append('\n');
if (replace) { // todo check this.equalsSize
// todo generated version comment
this.replaceLine(new SearchMetadata(this.importCollector, indent, line, i), content);
}
}
int startPatternIndex = line.indexOf(startPattern);
if (startPatternIndex != -1) {
if (!this.framed) {
this.framed = true;
indent = " ".repeat(startPatternIndex); // update indent based on the comments for flexibility
if (indent.length() % INDENT_UNIT.length() != 0) {
throw new IllegalStateException("Start generated comment is not properly indented at line " + (i + 1));
}
if (this.equalsSize) {
replace = true;
}
} else {
throw new IllegalStateException("Nested generated comments are not allowed for " + this.rewriteClass.getSimpleName() + " at line " + (i + 1));
}
}
}
}
if (this.framed) {
throw new IllegalStateException("End generated comment missing for " + this.rewriteClass.getSimpleName());
}
// often content doesn't match because of javadoc or by design for some rewriter so insert manually later
if (!this.equalsSize) {
StringBuilder replacedContent = new StringBuilder();
String[] stripLines = content.toString().split("\n");
boolean replace = false;
for (int i = 0; i < stripLines.length; i++) {
String line = stripLines[i];
if (replace) {
replacedContent.append(indent).append("// %s %s".formatted(
Annotations.annotationStyle(GeneratedFrom.class),
SharedConstants.getCurrentVersion().getName()
));
replacedContent.append('\n');
this.insert(new SearchMetadata(this.importCollector, indent, strippedContent.toString(), i), replacedContent);
replace = false;
Optional<String> startPattern = this.searchPattern(line, null, PAPER_START_FORMAT, patterns);
if (startPattern.isPresent()) {
if (this.foundRewriter != null) {
throw new IllegalStateException("Nested generated comments are not allowed for " + this.rewriteClass.getSimpleName() + " at line " + (i + 1));
}
replacedContent.append(line);
replacedContent.append('\n');
if (line.equals(indent + startPattern)) {
replace = true;
int startPatternIndex = line.indexOf(GENERATED_COMMENT_FORMAT.formatted(PAPER_START_FORMAT, startPattern.get()));
indent = " ".repeat(startPatternIndex); // update indent based on the comments for flexibility
if (indent.length() % INDENT_UNIT.length() != 0) {
throw new IllegalStateException("Start generated comment is not properly indented at line " + (i + 1));
}
this.foundRewriter = patternInfo.get(startPattern.get());
foundPatterns.add(this.foundRewriter.pattern);
if (!this.foundRewriter.equalsSize) {
strippedContent = new StringBuilder();
}
}
content = replacedContent;
i++;
}
if (this.foundRewriter != null) {
throw new IllegalStateException("End generated comment " + this.foundRewriter.pattern + " missing for " + this.rewriteClass.getSimpleName());
}
Set<String> diff = Sets.difference(patterns, foundPatterns);
if (!diff.isEmpty()) {
throw new IllegalStateException("SRT didn't found some expected generated comments: " + diff.toString());
}
return content;
}
@Override
public void writeToFile(Path parent) throws IOException {
String filePath = "%s/%s.java".formatted(
this.rewriteClass.getPackageName().replace('.', '/'),
Formatting.retrieveFileName(this.rewriteClass)
ClassHelper.getRootClass(this.rewriteClass).getSimpleName()
);
Path path = parent.resolve(filePath);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
StringBuilder content = this.searchAndReplace(lines, new StringBuilder());
StringBuilder content = new StringBuilder();
try (BufferedReader buffer = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
this.searchAndReplace(buffer, content);
}
// Files.writeString(path, content.toString(), StandardCharsets.UTF_8); // todo
Path createdPath = Main.generatedPath.resolve(filePath);
Files.createDirectories(createdPath.getParent());
Files.writeString(createdPath, content.toString(), StandardCharsets.UTF_8);
}
private void appendGeneratedComment(StringBuilder builder, String indent) {
builder.append(indent).append("// %s %s".formatted(
Annotations.annotationStyle(GeneratedFrom.class),
SharedConstants.getCurrentVersion().getName()
));
builder.append('\n');
}
private Optional<String> searchPattern(String rawLine, @Nullable String indent, String prefix, Set<String> patterns) {
boolean strict = indent != null;
String line = strict ? rawLine : rawLine.stripLeading();
for (String pattern : patterns) {
String comment = GENERATED_COMMENT_FORMAT.formatted(prefix, pattern);
if (strict ? line.equals(indent + comment) : line.equals(comment)) {
return Optional.of(pattern);
}
}
return Optional.empty();
}
}

View File

@ -1,10 +1,8 @@
package io.papermc.generator.rewriter.types;
import com.google.common.base.Preconditions;
import io.papermc.generator.rewriter.SearchMetadata;
import java.util.Arrays;
public class EnumCloneRewriter<T extends Enum<T>, A extends Enum<A>> extends EnumRewriter<T, A> {
public class EnumCloneRewriter<T extends Enum<T>, A extends Enum<A>> extends EnumRewriter<T, A> { // not really a clone anymore
private final Class<T> basedOn;
@ -22,11 +20,4 @@ public class EnumCloneRewriter<T extends Enum<T>, A extends Enum<A>> extends Enu
protected String rewriteEnumName(final T item) {
return item.name();
}
@Override
protected void insert(final SearchMetadata metadata, final StringBuilder builder) {
Preconditions.checkState(metadata.replacedContent().stripTrailing().endsWith(";"), "The generated comments must enclose the whole enum in the clone enum rewriter");
super.insert(metadata, builder);
}
}

View File

@ -77,8 +77,7 @@ public class RegistryFieldRewriter<T, A> extends SearchReplaceRewriter {
@Override
protected void insert(final SearchMetadata metadata, final StringBuilder builder) {
List<Holder.Reference<T>> references = this.registry.holders().sorted(Formatting.alphabeticKeyOrder(reference -> reference.key().location().getPath())).toList();
Iterator<Holder.Reference<T>> referenceIterator = references.iterator();
Iterator<Holder.Reference<T>> referenceIterator = this.registry.holders().sorted(Formatting.alphabeticKeyOrder(reference -> reference.key().location().getPath())).iterator();
while (referenceIterator.hasNext()) {
Holder.Reference<T> reference = referenceIterator.next();

View File

@ -58,8 +58,7 @@ public class TagRewriter extends SearchReplaceRewriter {
builder.append('\n');
builder.append('\n');
List<? extends TagKey<?>> tagKeys = registry.getTagNames().sorted(Formatting.alphabeticKeyOrder(tagKey -> tagKey.location().getPath())).toList();
Iterator<? extends TagKey<?>> keyIterator = tagKeys.iterator();
Iterator<? extends TagKey<?>> keyIterator = registry.getTagNames().sorted(Formatting.alphabeticKeyOrder(tagKey -> tagKey.location().getPath())).iterator();
while (keyIterator.hasNext()) {
TagKey<?> tagKey = keyIterator.next();

View File

@ -16,7 +16,7 @@ public final class Annotations {
}
public static String annotationStyle(Class<? extends Annotation> clazz) {
return "@%s".formatted(ImportCollector.retrieveFullNestedName(clazz));
return "@%s".formatted(ClassHelper.retrieveFullNestedName(clazz));
}
public static String annotation(Class<? extends Annotation> clazz, ImportCollector collector, String param, String value) {

View File

@ -0,0 +1,41 @@
package io.papermc.generator.rewriter.utils;
public final class ClassHelper {
public static String getDeclaredType(Class<?> clazz) {
if (clazz.isAnnotation()) {
return "@interface";
}
if (clazz.isInterface()) {
return "interface";
}
if (clazz.isEnum()) {
return "enum";
}
if (clazz.isRecord()) {
return "record";
}
return "class";
}
public static Class<?> getRootClass(Class<?> clazz) {
Class<?> rootClass = clazz;
Class<?> parentClass = clazz;
while (true) {
parentClass = parentClass.getEnclosingClass();
if (parentClass == null) {
break;
}
rootClass = parentClass;
}
return rootClass;
}
public static String retrieveFullNestedName(Class<?> clazz) {
String fqn = clazz.getCanonicalName();
return fqn.substring(clazz.getPackageName().length() + 1);
}
private ClassHelper() {
}
}

View File

@ -1,5 +1,6 @@
package io.papermc.generator.rewriter.utils;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -19,6 +20,7 @@ public class ImportCollector {
this.rewriteClass = rewriteClass;
}
@VisibleForTesting
public void addImport(String fqn) {
if (fqn.endsWith("*")) {
this.globalImports.add(fqn.substring(0, fqn.lastIndexOf('.')));
@ -27,6 +29,7 @@ public class ImportCollector {
}
}
@VisibleForTesting
public void addStaticImport(String fqn) { // todo support star import?
this.staticImports.put(fqn, fqn.substring(fqn.lastIndexOf('.') + 1));
}
@ -40,22 +43,13 @@ public class ImportCollector {
return this.typeCache.get(clazz);
}
Class<?> rootClass = clazz;
Class<?> parentClass = clazz;
while (true) {
parentClass = parentClass.getEnclosingClass();
if (parentClass == null) {
break;
}
rootClass = parentClass;
}
Class<?> rootClass = ClassHelper.getRootClass(clazz);
final String typeName;
if (this.rewriteClass == clazz ||
this.imports.contains(rootClass.getName()) ||
clazz.getPackageName().equals(this.rewriteClass.getPackageName()) || // same package don't need fqn too
this.globalImports.contains(clazz.getPackageName())) { // star import
typeName = retrieveFullNestedName(clazz);
typeName = ClassHelper.retrieveFullNestedName(clazz);
} else {
typeName = clazz.getCanonicalName();
}
@ -64,17 +58,21 @@ public class ImportCollector {
return typeName;
}
public void consume(String line) {
// precondition for inlined import and other like import 1; import 2; | ;;; import a ;;; + extra space
if (line.startsWith("import static ")) {
addStaticImport(line.substring("import static ".length(), line.length() - 1));
private void addImportLine(String importLine) {
if (importLine.startsWith("import static ")) {
addStaticImport(importLine.substring("import static ".length()));
} else {
addImport(line.substring("import ".length(), line.length() - 1));
addImport(importLine.substring("import ".length()));
}
}
public static String retrieveFullNestedName(Class<?> clazz) {
String fqn = clazz.getCanonicalName();
return fqn.substring(clazz.getPackageName().length() + 1);
public void consume(String line) {
for (String rawImport : line.split(";")) {
String importLine = rawImport.trim();
if (importLine.isEmpty()) {
continue;
}
addImportLine(importLine);
}
}
}

View File

@ -36,7 +36,6 @@ import org.checkerframework.framework.qual.DefaultQualifier;
import static com.squareup.javapoet.TypeSpec.interfaceBuilder;
import static io.papermc.generator.utils.Annotations.NOT_NULL;
import static io.papermc.generator.utils.Annotations.NULLABLE;
import static io.papermc.generator.utils.Annotations.experimentalAnnotations;
import static io.papermc.generator.utils.TagRegistry.registry;
import static javax.lang.model.element.Modifier.ABSTRACT;
@ -128,8 +127,7 @@ public class TagGenerator extends SimpleGenerator {
.initializer("$T.getTag($L, $T.minecraft($S), $T.class)", Bukkit.class, registryFieldName, NamespacedKey.class, keyPath, tagRegistry.apiType())
.addJavadoc(Javadocs.getVersionDependentField("{@code $L}"), tagKey.location().toString());
if (experimentalTags.contains(keyPath)) {
fieldBuilder.addAnnotations(experimentalAnnotations(Formatting.formatFeatureFlagName(Main.EXPERIMENTAL_TAGS.perFeatureFlag().get(tagKey))))
.addAnnotation(NULLABLE);
fieldBuilder.addAnnotations(experimentalAnnotations(Formatting.formatFeatureFlagName(Main.EXPERIMENTAL_TAGS.perFeatureFlag().get(tagKey))));
}
typeBuilder.addField(fieldBuilder.build());
});

View File

@ -49,7 +49,6 @@ public final class Annotations {
@ApiStatus.Experimental
public static final AnnotationSpec EXPERIMENTAL_API_ANNOTATION = AnnotationSpec.builder(ApiStatus.Experimental.class).build();
public static final AnnotationSpec NOT_NULL = AnnotationSpec.builder(NotNull.class).build();
public static final AnnotationSpec NULLABLE = AnnotationSpec.builder(Nullable.class).build();
public static final AnnotationSpec OVERRIDE = AnnotationSpec.builder(Override.class).build();
private static final AnnotationSpec SUPPRESS_WARNINGS = AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "unused")

View File

@ -80,16 +80,6 @@ public final class Formatting {
return indentBuilder.toString();
}
public static String retrieveFileName(Class<?> clazz) {
String name = clazz.getSimpleName();
Class<?> parent = clazz.getEnclosingClass();
while (parent != null) {
name = parent.getSimpleName();
parent = parent.getEnclosingClass();
}
return name;
}
public static String quoted(String value) {
return String.format("\"%s\"", value);
}

View File

@ -374,7 +374,7 @@ index 5fef0f8b7957d5daef864de8547104f552215f29..de742b029409e2c4d74f854ca802130d
@NotNull
private static DamageType getDamageType(@NotNull String key) {
diff --git a/src/main/java/org/bukkit/entity/Boat.java b/src/main/java/org/bukkit/entity/Boat.java
index 2ac685fb1817f3ce06ebe6391cc863712d68367c..3ce5fa588e46b251c459f2e191539377f946f287 100644
index 2ac685fb1817f3ce06ebe6391cc863712d68367c..056d44476b2118b124ad2f7262aee8631d1fc0aa 100644
--- a/src/main/java/org/bukkit/entity/Boat.java
+++ b/src/main/java/org/bukkit/entity/Boat.java
@@ -136,6 +136,7 @@ public interface Boat extends Vehicle {
@ -393,6 +393,20 @@ index 2ac685fb1817f3ce06ebe6391cc863712d68367c..3ce5fa588e46b251c459f2e191539377
private final Material materialBlock;
@@ -170,11 +172,13 @@ public interface Boat extends Vehicle {
public enum Status {
NOT_IN_WORLD, // Paper
+ // Paper start - Generated/BoatStatus
IN_WATER,
UNDER_WATER,
UNDER_FLOWING_WATER,
ON_LAND,
IN_AIR;
+ // Paper end - Generated/BoatStatus
}
// Paper start
diff --git a/src/main/java/org/bukkit/entity/Cat.java b/src/main/java/org/bukkit/entity/Cat.java
index d03adfaa4176617ef2ace2754fe02b63860e3aee..fceb2f0a452a1ce72988bbd1ecca599cfb6af6d9 100644
--- a/src/main/java/org/bukkit/entity/Cat.java