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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user