9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2026-01-04 15:41:38 +00:00

Merge pull request #113 from iqtesterrr/dev

feat: Re-implement WorldEdit command and suggestion support in a proper way
This commit is contained in:
XiaoMoMi
2025-04-19 02:01:29 +08:00
committed by GitHub
5 changed files with 271 additions and 102 deletions

View File

@@ -1,24 +1,102 @@
package net.momirealms.craftengine.bukkit.compatibility.worldedit;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.bukkit.BukkitBlockRegistry;
import com.sk89q.worldedit.extension.input.ParserContext;
import com.sk89q.worldedit.internal.registry.InputParser;
import com.sk89q.worldedit.util.concurrency.LazyReference;
import com.sk89q.worldedit.world.block.BaseBlock;
import com.sk89q.worldedit.world.block.BlockType;
import com.sk89q.worldedit.world.block.BlockTypes;
import net.momirealms.craftengine.core.block.AbstractBlockManager;
import net.momirealms.craftengine.core.block.BlockStateParser;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ReflectionUtils;
import org.bukkit.Material;
import java.lang.reflect.Field;
import java.util.Set;
import java.util.stream.Stream;
public class WorldEditBlockRegister {
private static final Field field$BlockType$blockMaterial;
private final Field field$BlockType$blockMaterial;
private final AbstractBlockManager manager;
static {
public WorldEditBlockRegister(AbstractBlockManager manager) {
field$BlockType$blockMaterial = ReflectionUtils.getDeclaredField(BlockType.class, "blockMaterial");
this.manager = manager;
CEBlockParser blockParser = new CEBlockParser(WorldEdit.getInstance());
WorldEdit.getInstance().getBlockFactory().register(blockParser);
}
public static void register(Key id) throws ReflectiveOperationException {
public void register(Key id) throws ReflectiveOperationException {
BlockType blockType = new BlockType(id.toString(), blockState -> blockState);
field$BlockType$blockMaterial.set(blockType, LazyReference.from(() -> new BukkitBlockRegistry.BukkitBlockMaterial(null, Material.STONE)));
BlockType.REGISTRY.register(id.toString(), blockType);
}
private final class CEBlockParser extends InputParser<BaseBlock> {
private CEBlockParser(WorldEdit worldEdit) {
super(worldEdit);
}
@Override
public Stream<String> getSuggestions(String input) {
Set<String> namespacesInUse = manager.namespacesInUse();
if (input.isEmpty() || input.equals(":")) {
return namespacesInUse.stream().map(namespace -> namespace + ":");
}
if (input.startsWith(":")) {
String term = input.substring(1);
return BlockStateParser.fillSuggestions(term).stream();
}
if (!input.contains(":")) {
String lowerSearch = input.toLowerCase();
return Stream.concat(
namespacesInUse.stream().filter(n -> n.startsWith(lowerSearch)).map(n -> n + ":"),
BlockStateParser.fillSuggestions(input).stream()
);
}
return BlockStateParser.fillSuggestions(input).stream();
}
@Override
public BaseBlock parseFromInput(String input, ParserContext context) {
int index = input.indexOf("[");
if (input.charAt(index+1) == ']') return null;
int colonIndex = input.indexOf(':');
if (colonIndex == -1) return null;
Set<String> namespacesInUse = manager.namespacesInUse();
String namespace = input.substring(0, colonIndex);
if (!namespacesInUse.contains(namespace)) return null;
ImmutableBlockState state = BlockStateParser.deserialize(input);
if (state == null) return null;
try {
String id = state.customBlockState().handle().toString();
int first = id.indexOf('{');
int last = id.indexOf('}');
if (first != -1 && last != -1 && last > first) {
String blockId = id.substring(first + 1, last);
BlockType blockType = BlockTypes.get(blockId);
if (blockType == null) {
return null;
}
return blockType.getDefaultState().toBaseBlock();
} else {
throw new IllegalArgumentException("Invalid block ID format: " + id);
}
} catch (NullPointerException e) {
return null;
}
}
}
}

View File

@@ -87,7 +87,6 @@ public class BukkitBlockManager extends AbstractBlockManager {
// Event listeners
private final BlockEventListener blockEventListener;
private final FallingBlockRemoveListener fallingBlockRemoveListener;
private WorldEditCommandHelper weCommandHelper;
public BukkitBlockManager(BukkitCraftEngine plugin) {
super(plugin);
@@ -128,18 +127,11 @@ public class BukkitBlockManager extends AbstractBlockManager {
if (this.fallingBlockRemoveListener != null) {
Bukkit.getPluginManager().registerEvents(this.fallingBlockRemoveListener, plugin.bootstrap());
}
boolean hasWE = false;
// WorldEdit
if (this.plugin.isPluginEnabled("FastAsyncWorldEdit")) {
this.initFastAsyncWorldEditHook();
hasWE = true;
} else if (this.plugin.isPluginEnabled("WorldEdit")) {
this.initWorldEditHook();
hasWE = true;
}
if (hasWE) {
this.weCommandHelper = new WorldEditCommandHelper(this.plugin, this);
this.weCommandHelper.enable();
}
}
@@ -159,7 +151,6 @@ public class BukkitBlockManager extends AbstractBlockManager {
this.unload();
HandlerList.unregisterAll(this.blockEventListener);
if (this.fallingBlockRemoveListener != null) HandlerList.unregisterAll(this.fallingBlockRemoveListener);
if (this.weCommandHelper != null) this.weCommandHelper.disable();
}
@Override
@@ -181,13 +172,14 @@ public class BukkitBlockManager extends AbstractBlockManager {
}
public void initFastAsyncWorldEditHook() {
// do nothing
new WorldEditBlockRegister(this);
}
public void initWorldEditHook() {
WorldEditBlockRegister weBlockRegister = new WorldEditBlockRegister(this);
try {
for (Key newBlockId : this.blockRegisterOrder) {
WorldEditBlockRegister.register(newBlockId);
weBlockRegister.register(newBlockId);
}
} catch (Exception e) {
this.plugin.logger().warn("Failed to initialize world edit hook", e);

View File

@@ -1,85 +0,0 @@
package net.momirealms.craftengine.bukkit.block;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.core.block.BlockStateParser;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
// TODO A better command suggestion system
public class WorldEditCommandHelper implements Listener {
private final BukkitBlockManager manager;
private final BukkitCraftEngine plugin;
public WorldEditCommandHelper(BukkitCraftEngine plugin, BukkitBlockManager manager) {
this.plugin = plugin;
this.manager = manager;
}
public void enable() {
Bukkit.getPluginManager().registerEvents(this, plugin.bootstrap());
}
public void disable() {
HandlerList.unregisterAll(this);
}
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
String message = event.getMessage();
if (!message.startsWith("//")) return;
Set<String> cachedNamespaces = manager.namespacesInUse();
String[] args = message.split(" ");
boolean modified = false;
for (int i = 1; i < args.length; i++) {
String[] parts = args[i].split(",");
List<String> processedParts = new ArrayList<>(parts.length);
boolean partModified = false;
for (String part : parts) {
String processed = processIdentifier(part, cachedNamespaces);
partModified |= !part.equals(processed);
processedParts.add(processed);
}
if (partModified) {
args[i] = String.join(",", processedParts);
modified = true;
}
}
if (modified) {
event.setMessage(String.join(" ", args));
}
}
private String processIdentifier(String identifier, Set<String> cachedNamespaces) {
int colonIndex = identifier.indexOf(':');
if (colonIndex == -1) return identifier;
String namespace = identifier.substring(0, colonIndex);
if (!cachedNamespaces.contains(namespace)) return identifier;
ImmutableBlockState state = BlockStateParser.deserialize(identifier);
if (state == null) return identifier;
try {
return BlockStateUtils.getBlockOwnerIdFromState(
state.customBlockState().handle()
).toString();
} catch (NullPointerException e) {
return identifier;
}
}
}

View File

@@ -19,6 +19,7 @@ import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class DebugSetBlockCommand extends BukkitCommandFeature<CommandSender> {
@@ -33,7 +34,7 @@ public class DebugSetBlockCommand extends BukkitCommandFeature<CommandSender> {
.required("id", StringParser.stringComponent(StringParser.StringMode.GREEDY_FLAG_YIELDING).suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().blockManager().cachedSuggestions());
return CompletableFuture.completedFuture(BlockStateParser.fillSuggestions(input.input(), input.cursor()).stream().map(Suggestion::suggestion).collect(Collectors.toList()));
}
}))
.handler(context -> {

View File

@@ -8,9 +8,183 @@ import net.momirealms.craftengine.core.util.StringReader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
public class BlockStateParser {
private static final char START = '[';
private static final char EQUAL = '=';
private static final char SEPARATOR = ',';
private static final char END = ']';
private final StringReader reader;
private final int cursor;
private final Set<String> suggestions = new HashSet<>();
private final Set<String> used = new HashSet<>();
private String input;
private int replaceCursor;
private Holder<CustomBlock> block;
private Collection<Property<?>> properties;
private Property<?> property;
public BlockStateParser(String data, int cursor) {
this.reader = new StringReader(data.toLowerCase());
this.reader.setCursor(cursor);
this.cursor = cursor;
this.replaceCursor = cursor;
}
public static Set<String> fillSuggestions(@NotNull String data) {
return fillSuggestions(data, 0);
}
public static Set<String> fillSuggestions(@NotNull String data, int cursor) {
BlockStateParser parser = new BlockStateParser(data, cursor);
parser.parse();
return parser.suggestions;
}
private void parse() {
readBlock();
if (block == null) {
suggestBlock();
return;
}
readProperties();
if (properties.isEmpty()) return;
if (!reader.canRead())
suggestStart();
else if (reader.peek() == START) {
reader.skip();
suggestProperties();
}
}
private void readBlock() {
this.replaceCursor = reader.getCursor();
this.input = reader.readUnquotedString();
if (reader.canRead() && reader.peek() == ':') {
reader.skip();
input = input + ":" + reader.readUnquotedString();
}
BuiltInRegistries.BLOCK.get(Key.from(input)).ifPresent(block -> this.block = block);
}
private void suggestBlock() {
String front = readPrefix();
for (Key key : BuiltInRegistries.BLOCK.keySet()) {
String id = key.toString();
if (id.contains(input)) {
this.suggestions.add(front + id);
}
}
this.suggestions.remove(front + "craftengine:empty");
}
private void readProperties() {
this.properties = this.block.value().properties();
}
private void suggestStart() {
this.replaceCursor = reader.getCursor();
this.suggestions.add(readPrefix() + START);
}
private void suggestProperties() {
this.reader.skipWhitespace();
this.replaceCursor = reader.getCursor();
suggestPropertyNameAndEnd();
while (reader.canRead()) {
if (used.isEmpty() && reader.peek() == SEPARATOR) return;
if (reader.peek() == SEPARATOR) reader.skip();
reader.skipWhitespace();
if (reader.canRead() && reader.peek() == END) return;
replaceCursor = reader.getCursor();
input = reader.readString();
property = block.value().getProperty(input);
if (property == null) {
suggestPropertyName();
return;
}
if (used.contains(property.name().toLowerCase())) return;
used.add(input);
reader.skipWhitespace();
replaceCursor = reader.getCursor();
suggestEqual();
if (!reader.canRead() || reader.peek() != EQUAL) return;
reader.skip();
reader.skipWhitespace();
replaceCursor = reader.getCursor();
input = reader.readString();
if (property.possibleValues().stream().noneMatch
(value -> value.toString().equalsIgnoreCase(input))
){
suggestValue();
return;
}
reader.skipWhitespace();
replaceCursor = reader.getCursor();
if (reader.canRead()) {
if (used.size() == properties.size()) return;
if (reader.peek() != SEPARATOR) return;
} else if (used.size() < properties.size()) {
suggestSeparator();
}
}
suggestEnd();
}
private void suggestPropertyNameAndEnd() {
if (!reader.getRemaining().isEmpty()) return;
this.input = "";
suggestEnd();
suggestPropertyName();
}
private void suggestPropertyName() {
if (!reader.getRemaining().isEmpty()) return;
String front = readPrefix();
for (Property<?> p : properties) {
if (!used.contains(p.name().toLowerCase()) && p.name().toLowerCase().startsWith(input)) {
this.suggestions.add(front + p.name() + EQUAL);
}
}
}
private void suggestEqual() {
if (!reader.getRemaining().isEmpty()) return;
this.suggestions.add(readPrefix() + EQUAL);
}
private void suggestValue() {
for (Object val : property.possibleValues()) {
this.suggestions.add(readPrefix() + val.toString().toLowerCase());
}
}
private void suggestSeparator() {
this.suggestions.add(readPrefix() + SEPARATOR);
}
private void suggestEnd() {
this.suggestions.add(readPrefix() + END);
}
private String readPrefix() {
return reader.getString().substring(cursor, replaceCursor);
}
@Nullable
public static ImmutableBlockState deserialize(@NotNull String data) {
@@ -28,26 +202,35 @@ public class BlockStateParser {
ImmutableBlockState defaultState = holder.value().defaultState();
if (reader.canRead() && reader.peek() == '[') {
reader.skip();
while (reader.canRead() && reader.peek() != ']') {
while (reader.canRead()) {
reader.skipWhitespace();
if (reader.peek() == ']') break;
String propertyName = reader.readUnquotedString();
reader.skipWhitespace();
if (!reader.canRead() || reader.peek() != '=') {
return null;
}
reader.skip();
reader.skipWhitespace();
String propertyValue = reader.readUnquotedString();
Property<?> property = holder.value().getProperty(propertyName);
if (property != null) {
Optional<?> optionalValue = property.optional(propertyValue);
if (optionalValue.isEmpty()) {
defaultState = ImmutableBlockState.with(defaultState, property, property.defaultValue());
//defaultState = ImmutableBlockState.with(defaultState, property, property.defaultValue());
return null;
} else {
defaultState = ImmutableBlockState.with(defaultState, property, optionalValue.get());
}
} else {
return null;
}
reader.skipWhitespace();
if (reader.canRead() && reader.peek() == ',') {
reader.skip();
}
}
reader.skipWhitespace();
if (reader.canRead() && reader.peek() == ']') {
reader.skip();
} else {