Fix NBT editor paste and delete shortcuts

This commit is contained in:
ms2k 2026-11-09 22:08:00 +09:00
commit a45c574595
3 changed files with 120 additions and 11 deletions

View file

@ -0,0 +1,52 @@
package dev.quorate.client.nbt;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.TagParser;
public final class ClipboardParser extends TagParser {
private final StringReader input;
private ClipboardParser(StringReader input) {
super(input);
this.input = input;
}
public static CompoundTag parse(String text) throws CommandSyntaxException {
StringReader input = new StringReader(text);
CompoundTag result = new ClipboardParser(input).readStruct();
input.skipWhitespace();
if (input.canRead()) throw ERROR_TRAILING_DATA.createWithContext(input);
return result;
}
@Override
public CompoundTag readStruct() throws CommandSyntaxException {
expect('{');
CompoundTag result = new CompoundTag();
input.skipWhitespace();
while (input.canRead() && input.peek() != '}') {
int start = input.getCursor();
String key = readKey();
if (key.isEmpty() && input.getCursor() == start) {
throw ERROR_EXPECTED_KEY.createWithContext(input);
}
expect(':');
result.put(key, readValue());
input.skipWhitespace();
if (!input.canRead() || input.peek() != ',') break;
input.skip();
input.skipWhitespace();
if (!input.canRead()) throw ERROR_EXPECTED_KEY.createWithContext(input);
}
expect('}');
return result;
}
private void expect(char character) throws CommandSyntaxException {
input.skipWhitespace();
input.expect(character);
}
}

View file

@ -7,11 +7,12 @@ import dev.ftb.mods.ftblibrary.nbtedit.NBTEditorScreen.ButtonNBT;
import dev.ftb.mods.ftblibrary.nbtedit.NBTEditorScreen.ButtonNBTCollection;
import dev.ftb.mods.ftblibrary.nbtedit.NBTEditorScreen.ButtonNBTMap;
import dev.ftb.mods.ftblibrary.ui.input.Key;
import dev.ftb.mods.ftblibrary.ui.misc.SimpleToast;
import dev.quorate.client.nbt.ClipboardParser;
import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.TagParser;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
@ -22,6 +23,8 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
@Mixin(value = NBTEditorScreen.class, remap = false)
public abstract class EditorMixin {
@Shadow private ButtonNBT selected;
@ -33,14 +36,17 @@ public abstract class EditorMixin {
@Inject(method = "keyPressed", at = @At("RETURN"), cancellable = true)
private void saltatory$keys(Key key, CallbackInfoReturnable<Boolean> result) {
if (result.getReturnValue() || !key.modifiers.control()) return;
if (result.getReturnValue() || (!key.modifiers.control() && !key.is(GLFW.GLFW_KEY_DELETE)))
return;
if (key.is(GLFW.GLFW_KEY_C)) {
saltatory$copy();
result.setReturnValue(true);
} else if (key.is(GLFW.GLFW_KEY_V)) {
saltatory$paste();
result.setReturnValue(true);
} else if (key.is(GLFW.GLFW_KEY_D) || key.is(GLFW.GLFW_KEY_X)) {
} else if (key.is(GLFW.GLFW_KEY_DELETE)
|| key.is(GLFW.GLFW_KEY_D)
|| key.is(GLFW.GLFW_KEY_X)) {
NodeAccess node = (NodeAccess) selected;
ButtonNBTCollection parent = node.saltatory$parent();
if (parent != null) {
@ -74,11 +80,10 @@ public abstract class EditorMixin {
if (!(selected instanceof ButtonNBTCollection target)) return;
CompoundTag copied;
try {
copied = TagParser.parseTag(Minecraft.getInstance().keyboardHandler.getClipboard());
copied = ClipboardParser.parse(Minecraft.getInstance().keyboardHandler.getClipboard());
} catch (CommandSyntaxException e) {
Minecraft.getInstance()
.player
.displayClientMessage(Component.literal("Invalid NBT clipboard"), true);
SimpleToast.error(
Component.literal("Invalid NBT clipboard"), Component.literal(e.getMessage()));
return;
}
byte elementType = 0;
@ -88,18 +93,33 @@ public abstract class EditorMixin {
|| (!(target instanceof ButtonNBTMap)
&& elementType != 0
&& elementType != tag.getId())) {
Minecraft.getInstance()
.player
.displayClientMessage(
Component.literal("NBT type does not fit selection"), true);
SimpleToast.error(
Component.literal("NBT type does not fit selection"), Component.empty());
return;
}
elementType = tag.getId();
}
Map<String, ButtonNBT> previous = Map.copyOf(target.children);
for (String name : copied.getAllKeys()) {
target.setTag(target instanceof ButtonNBTMap ? name : "-1", copied.get(name).copy());
}
target.updateChildren(false);
saltatory$restore(previous, target);
target.getGui().refreshWidgets();
}
@Unique
private static void saltatory$restore(
Map<String, ButtonNBT> previous, ButtonNBTCollection target) {
for (var entry : target.children.entrySet()) {
ButtonNBT old = previous.get(entry.getKey());
ButtonNBT current = entry.getValue();
if (old instanceof ButtonNBTCollection oldTree
&& current instanceof ButtonNBTCollection newTree
&& old.getClass() == current.getClass()) {
newTree.setCollapsed(oldTree.collapsed);
saltatory$restore(oldTree.children, newTree);
}
}
}
}

View file

@ -0,0 +1,37 @@
package dev.quorate.client.nbt;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.TagParser;
import org.junit.jupiter.api.Test;
final class ClipboardParserTest {
private static final String EMPTY_NAMES = "{outer:{args:[{name:'value'},{'':''},{'':''}]}}";
@Test
void readsEmptyNames() throws CommandSyntaxException {
assertThrows(CommandSyntaxException.class, () -> TagParser.parseTag(EMPTY_NAMES));
CompoundTag parsed = ClipboardParser.parse(EMPTY_NAMES);
var args = parsed.getCompound("outer").getList("args", Tag.TAG_COMPOUND);
assertEquals(3, args.size());
assertTrue(args.getCompound(1).contains("", Tag.TAG_STRING));
assertEquals("", args.getCompound(1).getString(""));
assertEquals(parsed, ClipboardParser.parse(parsed.toString()));
}
@Test
void keepsNormalSyntax() throws CommandSyntaxException {
String text = "{a:[1,2,3],b:{nested:'value'},c:[B;1b,2b]}";
assertEquals(TagParser.parseTag(text), ClipboardParser.parse(text));
for (String invalid : new String[] {"{:1}", "{a:1} trailing", "{a:[1,2b]}", "{a:1,"}) {
assertThrows(CommandSyntaxException.class, () -> ClipboardParser.parse(invalid));
}
}
}