diff --git a/README.md b/README.md index ed7b060..346e35c 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ This is the unfinished port of the WebDisplays mod for Minecraft 1.12.2. The tex * Peripheral: OpenComputers interface * Read config (see "Config elements" below) * Miniserv timeout -* Recipe for server block -* del command ### TODO * French translations diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiServer.java b/src/main/java/net/montoyo/wd/client/gui/GuiServer.java index 9b6ff75..b67d250 100644 --- a/src/main/java/net/montoyo/wd/client/gui/GuiServer.java +++ b/src/main/java/net/montoyo/wd/client/gui/GuiServer.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -38,7 +39,7 @@ public class GuiServer extends WDScreen { private static final ResourceLocation BG_IMAGE = new ResourceLocation("webdisplays", "textures/gui/server_bg.png"); private static final ResourceLocation FG_IMAGE = new ResourceLocation("webdisplays", "textures/gui/server_fg.png"); private static final HashMap COMMAND_MAP = new HashMap<>(); - private static final int MAX_LINE_LEN = 30; + private static final int MAX_LINE_LEN = 32; private static final int MAX_LINES = 12; private final NameUUIDPair owner; @@ -48,8 +49,9 @@ public class GuiServer extends WDScreen { private int blinkTime; private String lastCmd; private boolean promptLocked; - private long queryTime; + private volatile long queryTime; private ClientTask currentTask; + private int selectedLine = -1; //Access command private int accessTrials; @@ -59,10 +61,12 @@ public class GuiServer extends WDScreen { //Upload wizard private boolean uploadWizard; - private int selectedLine = -1; private File uploadDir; - private File[] uploadFiles; + private final ArrayList uploadFiles = new ArrayList<>(); private int uploadOffset; + private boolean uploadFirstIsParent; + private String uploadFilter = ""; + private long uploadFilterTime; public GuiServer(NameUUIDPair owner) { this.owner = owner; @@ -171,10 +175,22 @@ public class GuiServer extends WDScreen { } else { blinkTime = (blinkTime + 1) % 10; - if(currentTask != null && System.currentTimeMillis() - queryTime >= 10000) { - writeLine(tr("timeout")); - currentTask.cancel(); - clearTask(); + if(currentTask != null) { + long queryTime; + synchronized(this) { + queryTime = this.queryTime; + } + + if(System.currentTimeMillis() - queryTime >= 10000) { + writeLine(tr("timeout")); + currentTask.cancel(); + clearTask(); + } + } + + if(!uploadFilter.isEmpty() && System.currentTimeMillis() - uploadFilterTime >= 1000) { + Log.info("Upload filter cleared"); + uploadFilter = ""; } } } @@ -187,19 +203,46 @@ public class GuiServer extends WDScreen { if(uploadWizard) { if(keyState) { if(keyCode == Keyboard.KEY_UP) { - if(--selectedLine < 3) - selectedLine = MAX_LINES - 1; + if(selectedLine > 3) + selectedLine--; + else if(uploadOffset > 0) { + uploadOffset--; + updateUploadScreen(); + } } else if(keyCode == Keyboard.KEY_DOWN) { - if(++selectedLine >= MAX_LINES) - selectedLine = 3; + if(selectedLine < MAX_LINES - 1) + selectedLine++; + else if(uploadOffset + selectedLine - 2 < uploadFiles.size()) { + uploadOffset++; + updateUploadScreen(); + } + } else if(keyCode == Keyboard.KEY_PRIOR) { + selectedLine = 3; + int dst = uploadOffset - (MAX_LINES - 3); + if(dst < 0) + dst = 0; + + selectFile(dst); + } else if(keyCode == Keyboard.KEY_NEXT) { + selectedLine = 3; + int dst = uploadOffset + (MAX_LINES - 3); + if(dst >= uploadFiles.size()) + dst = uploadFiles.size() - 1; + + selectFile(dst); + } else if(keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { + File file = uploadFiles.get(uploadOffset + selectedLine - 3); + + if(file.isDirectory()) { + uploadCD(file); + updateUploadScreen(); + } else + startFileUpload(file, true); } } if(keyCode == Keyboard.KEY_ESCAPE) { - lines.clear(); - promptLocked = false; - uploadWizard = false; - selectedLine = -1; + quitUploadWizard(); return; //Don't let the screen handle this } @@ -208,9 +251,16 @@ public class GuiServer extends WDScreen { super.handleKeyboardInput(); if(keyState) { - if(keyCode == Keyboard.KEY_L && (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL))) + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + + if(keyCode == Keyboard.KEY_L && ctrl) lines.clear(); - else if(keyCode == Keyboard.KEY_UP) { + else if(keyCode == Keyboard.KEY_V && ctrl) { + prompt += getClipboardString(); + + if(prompt.length() > MAX_LINE_LEN) + prompt = prompt.substring(0, MAX_LINE_LEN); + } else if(keyCode == Keyboard.KEY_UP) { if(lastCmd != null) { String tmp = prompt; prompt = lastCmd; @@ -225,7 +275,24 @@ public class GuiServer extends WDScreen { protected void keyTyped(char typedChar, int keyCode) throws IOException { super.keyTyped(typedChar, keyCode); - if(promptLocked || uploadWizard) + if(uploadWizard) { + boolean found = false; + uploadFilter += Character.toLowerCase(typedChar); + uploadFilterTime = System.currentTimeMillis(); + + for(int i = uploadFirstIsParent ? 1 : 0; i < uploadFiles.size(); i++) { + if(uploadFiles.get(i).getName().toLowerCase().startsWith(uploadFilter)) { + selectFile(i); + found = true; + break; + } + } + + if(!found && uploadFilter.length() == 1) + uploadFilter = ""; + + return; + } else if(promptLocked) return; if(keyCode == Keyboard.KEY_BACK) { @@ -293,6 +360,13 @@ public class GuiServer extends WDScreen { } } + private void quitUploadWizard() { + lines.clear(); + promptLocked = false; + uploadWizard = false; + selectedLine = -1; + } + @Override public void onGuiClosed() { super.onGuiClosed(); @@ -304,7 +378,7 @@ public class GuiServer extends WDScreen { private boolean queueTask(ClientTask task) { if(Client.getInstance().addTask(task)) { promptLocked = true; - queryTime = System.currentTimeMillis(); + queryTime = System.currentTimeMillis(); //No task is running so it's okay to have an unsynchronized access here currentTask = task; return true; } else { @@ -336,9 +410,18 @@ public class GuiServer extends WDScreen { } @CommandHandler("help") - public void commandHelp() { - for(String c : COMMAND_MAP.keySet()) - writeLine(c + " - " + tr("help." + c)); + public void commandHelp(String[] args) { + if(args.length > 0) { + String cmd = args[0].toLowerCase(); + + if(COMMAND_MAP.containsKey(cmd)) + writeLine(tr("help." + cmd)); + else + writeLine(tr("unknowncmd")); + } else { + for(String c : COMMAND_MAP.keySet()) + writeLine(c + " - " + tr("help." + c)); + } } @CommandHandler("exit") @@ -408,7 +491,7 @@ public class GuiServer extends WDScreen { @CommandHandler("url") public void commandURL(String[] args) { if(args.length < 1) { - writeLine(tr("urlarg")); + writeLine(tr("fnamearg")); return; } @@ -442,11 +525,29 @@ public class GuiServer extends WDScreen { uploadDir = newDir; } - uploadFiles = uploadDir.listFiles(); - if(uploadFiles == null) - uploadFiles = new File[0]; - else - uploadFiles = Arrays.stream(uploadFiles).filter(f -> !f.isHidden() && (f.isDirectory() || (f.isFile() && !Util.isFileNameInvalid(f.getName())))).toArray(File[]::new); + uploadFiles.clear(); + File parent = uploadDir.getParentFile(); + + if(parent != null && parent.exists()) { + uploadFiles.add(parent); + uploadFirstIsParent = true; + } else + uploadFirstIsParent = false; + + File[] children = uploadDir.listFiles(); + if(children != null) { + Collator c = Collator.getInstance(); + c.setStrength(Collator.SECONDARY); + c.setDecomposition(Collator.CANONICAL_DECOMPOSITION); + + Arrays.stream(children).filter(f -> !f.isHidden() && (f.isDirectory() || f.isFile())).sorted((a, b) -> c.compare(a.getName(), b.getName())).forEach(uploadFiles::add); + } + + uploadOffset = 0; + uploadFilter = ""; + + if(uploadWizard) + selectedLine = 3; } private void updateUploadScreen() { @@ -455,20 +556,134 @@ public class GuiServer extends WDScreen { lines.add("Choose a file to upload"); lines.add(trimStringL(uploadDir.getPath())); lines.add(""); - lines.add("[Parent]"); - final int maxl = Math.min(MAX_LINES - 4, uploadFiles.length); - for(int i = uploadOffset; i < maxl; i++) - lines.add(trimStringR(uploadFiles[i].getName())); + for(int i = uploadOffset; i < uploadFiles.size() && lines.size() < MAX_LINES; i++) { + if(i == 0 && uploadFirstIsParent) + lines.add("[Parent directory]"); + else + lines.add(trimStringR(uploadFiles.get(i).getName())); + } + } + + private void selectFile(int i) { + int pos = 3 + i - uploadOffset; + if(pos >= 3 && pos < MAX_LINES) { + selectedLine = pos; + return; + } + + uploadOffset = i; + if(uploadOffset + MAX_LINES - 3 > uploadFiles.size()) + uploadOffset = uploadFiles.size() - MAX_LINES + 3; + + updateUploadScreen(); + selectedLine = 3 + i - uploadOffset; } @CommandHandler("upload") - public void commandUpload() { + public void commandUpload(String[] args) { + if(!mc.player.getGameProfile().getId().equals(owner.uuid)) { + writeLine(tr("errowner")); + return; + } + + if(args.length > 0) { + File fle = new File(Util.join(args, " ")); + if(!fle.exists()) { + writeLine(tr("notfound")); + return; + } + + if(fle.isDirectory()) + uploadCD(fle); + else if(fle.isFile()) { + startFileUpload(fle, false); + return; + } else { + writeLine(tr("notfound")); + return; + } + } + uploadWizard = true; promptLocked = true; - selectedLine = 3; uploadOffset = 0; + selectedLine = 3; updateUploadScreen(); } + @CommandHandler("rm") + public void commandDelete(String[] args) { + if(!mc.player.getGameProfile().getId().equals(owner.uuid)) { + writeLine(tr("errowner")); + return; + } + + if(args.length < 1) { + writeLine(tr("fnamearg")); + return; + } + + String fname = Util.join(args, " "); + if(Util.isFileNameInvalid(fname)) { + writeLine(tr("nameerr")); + return; + } + + ClientTaskDeleteFile task = new ClientTaskDeleteFile(fname); + task.setFinishCallback((t) -> { + int status = t.getStatus(); + if(status == 1) + writeLine(tr("notfound")); + else if(status != 0) + writeLine(tr("error")); + + clearTask(); + }); + + queueTask(task); + } + + private void startFileUpload(File f, boolean quit) { + if(quit) + quitUploadWizard(); + + if(Util.isFileNameInvalid(f.getName()) || f.getName().length() >= MAX_LINE_LEN - 3) { + writeLine(tr("nameerr")); + return; + } + + ClientTaskUploadFile task; + try { + task = new ClientTaskUploadFile(f); + } catch(IOException ex) { + writeLine(tr("error")); + ex.printStackTrace(); + return; + } + + task.setProgressCallback((cur, total) -> { + synchronized(GuiServer.this) { + queryTime = System.currentTimeMillis(); + } + }); + + task.setFinishCallback(t -> { + int status = t.getUploadStatus(); + if(status == 0) + writeLine(tr("upload.done")); + else if(status == Constants.FUPA_STATUS_FILE_EXISTS) + writeLine(tr("upload.exists")); + else if(status == Constants.FUPA_STATUS_EXCEEDS_QUOTA) + writeLine(tr("upload.quota")); + else + writeLine(tr("error2", status)); + + clearTask(); + }); + + if(queueTask(task)) + writeLine(tr("upload.uploading")); + } + } diff --git a/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java index 0b0b979..c9ab037 100644 --- a/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java +++ b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java @@ -46,11 +46,6 @@ public abstract class AbstractClient { packetHandlers[ph.value().ordinal()] = m; } } - - for(int i = 0; i < packetHandlers.length; i++) { - if(packetHandlers[i] == null) - Log.warning("AbstractClient: no packet handler for %s", PacketID.fromInt(i).toString()); - } } protected abstract void onWriteError(); @@ -67,17 +62,13 @@ public abstract class AbstractClient { if(pid >= packetHandlers.length) Log.error("Caught invalid packet ID %d", pid); - else { - Log.info("Received PID %s", PacketID.fromInt(pid).toString()); - - if(packetHandlers[pid] != null) { - try { - packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry - } catch(IllegalAccessException ex) { - Log.errorEx("This shouldn't have happened", ex); - } catch(InvocationTargetException ex) { - Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid); - } + else if(packetHandlers[pid] != null) { + try { + packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry + } catch(IllegalAccessException ex) { + Log.errorEx("This shouldn't have happened", ex); + } catch(InvocationTargetException ex) { + Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid); } } } catch(IOException ex) { diff --git a/src/main/java/net/montoyo/wd/miniserv/PacketID.java b/src/main/java/net/montoyo/wd/miniserv/PacketID.java index 569b5f7..82da2b9 100644 --- a/src/main/java/net/montoyo/wd/miniserv/PacketID.java +++ b/src/main/java/net/montoyo/wd/miniserv/PacketID.java @@ -14,7 +14,8 @@ public enum PacketID { FILE_STATUS, //S->C GET_FILE, //C->S QUOTA, //C->S and S->C - LIST; //C->S and S->C + LIST, //C->S and S->C + DELETE; //C->S and S->C public static PacketID fromInt(int i) { PacketID[] values = values(); diff --git a/src/main/java/net/montoyo/wd/miniserv/client/Client.java b/src/main/java/net/montoyo/wd/miniserv/client/Client.java index cd74178..edd8448 100644 --- a/src/main/java/net/montoyo/wd/miniserv/client/Client.java +++ b/src/main/java/net/montoyo/wd/miniserv/client/Client.java @@ -320,6 +320,12 @@ public class Client extends AbstractClient implements Runnable { ((ClientTaskGetFileList) currentTask).onFileList(files); } + @PacketHandler(PacketID.DELETE) + public void handleDelete(DataInputStream dis) throws IOException { + if(currentTask instanceof ClientTaskDeleteFile) + ((ClientTaskDeleteFile) currentTask).onStatusPacket(dis.readByte()); + } + public void nextTask() { if(currentTask != null) currentTask.onFinished(); diff --git a/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskDeleteFile.java b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskDeleteFile.java new file mode 100644 index 0000000..af24481 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskDeleteFile.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.client; + +import net.montoyo.wd.miniserv.OutgoingPacket; +import net.montoyo.wd.miniserv.PacketID; + +public class ClientTaskDeleteFile extends ClientTask { + + private final String fname; + private int status; + + public ClientTaskDeleteFile(String fname) { + this.fname = fname; + } + + @Override + public void start() { + OutgoingPacket pkt = new OutgoingPacket(); + pkt.writeByte(PacketID.DELETE.ordinal()); + pkt.writeString(fname); + client.sendPacket(pkt); + } + + @Override + public void abort() { + } + + public void onStatusPacket(int s) { + status = s; + client.nextTask(); + } + + public int getStatus() { + return status; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java index 5cc2f9e..c214e07 100644 --- a/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java +++ b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java @@ -14,6 +14,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; +import java.util.function.BiConsumer; import java.util.function.Consumer; public class ClientTaskUploadFile extends ClientTask implements Consumer { @@ -25,11 +26,14 @@ public class ClientTaskUploadFile extends ClientTask imple private FileInputStream fis; private boolean abortFupa; private int uploadStatus; + private long uploadPos; + private BiConsumer onProgress; public ClientTaskUploadFile(File fle) throws IOException { file = fle; size = Files.size(fle.toPath()); fis = new FileInputStream(fle); + runCallbackOnMcThread = true; } @Override @@ -45,7 +49,7 @@ public class ClientTaskUploadFile extends ClientTask imple @Override public void abort() { abortFupa = true; - setUploadStatus(Constants.FUPA_STATUS_CONNECTION_LOST); + uploadStatus = Constants.FUPA_STATUS_CONNECTION_LOST; Util.silentClose(fis); } @@ -56,14 +60,14 @@ public class ClientTaskUploadFile extends ClientTask imple accept(null); } else { Util.silentClose(fis); - setUploadStatus(status); + uploadStatus = status; client.nextTask(); } } public void onUploadFinishedStatus(int status) { abortFupa = true; //This isn't necessary, but just in case... - setUploadStatus(status); + uploadStatus = status; client.nextTask(); } @@ -91,6 +95,11 @@ public class ClientTaskUploadFile extends ClientTask imple pkt.writeBytes(UPLOAD_BUFFER, 0, rd); client.sendPacket(pkt); + if(onProgress != null) { + uploadPos += (long) rd; + onProgress.accept(uploadPos, size); + } + if(rd > 0) { pkt.setOnFinishAction(this); return; @@ -100,19 +109,12 @@ public class ClientTaskUploadFile extends ClientTask imple Util.silentClose(file); } - private void setUploadStatus(int val) { - synchronized(this) { - uploadStatus = val; - } + public int getUploadStatus() { + return uploadStatus; } - public int getUploadStatus() { - int ret; - synchronized(this) { - ret = uploadStatus; - } - - return ret; + public void setProgressCallback(BiConsumer onProgress) { + this.onProgress = onProgress; } } diff --git a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java index 3530ed0..557e0d7 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java @@ -13,6 +13,7 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; +import java.nio.file.Files; import java.util.Arrays; import java.util.UUID; import java.util.function.Consumer; @@ -278,6 +279,39 @@ public class ServerClient extends AbstractClient { sendPacket(pkt); } + @PacketHandler(PacketID.DELETE) + public void handleDelete(DataInputStream dis) throws IOException { + String fname = readString(dis); + int status = 2; + + if(!Util.isFileNameInvalid(fname)) { + File file = new File(userDir, fname); + + if(file.exists() && file.isFile()) { + try { + long sz = Files.size(file.toPath()); + + if(file.delete()) { + quota -= sz; + if(quota < 0) + quota = 0; + + saveQuota(); + status = 0; + } + } catch(IOException ex) { + Log.errorEx("Couldn't get size of file %s of user %s for removal", ex, file.getAbsolutePath(), uuid.toString()); + } + } else + status = 1; + } + + OutgoingPacket ret = new OutgoingPacket(); + ret.writeByte(PacketID.DELETE.ordinal()); + ret.writeByte(status); + sendPacket(ret); + } + private void finishUpload(int status) { if(currentFile != null) { OutgoingPacket pkt = new OutgoingPacket(); @@ -289,14 +323,17 @@ public class ServerClient extends AbstractClient { currentFile = null; quota += currentFileSize; + saveQuota(); + } + } - try { - DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File(userDir, ".quota"))); - dos.writeLong(quota); - Util.silentClose(dos); - } catch(IOException ex) { - Log.errorEx("Could not save quota data for user %s", ex, uuid.toString()); - } + private void saveQuota() { + try { + DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File(userDir, ".quota"))); + dos.writeLong(quota); + Util.silentClose(dos); + } catch(IOException ex) { + Log.errorEx("Could not save quota data for user %s", ex, uuid.toString()); } } diff --git a/src/main/resources/assets/webdisplays/advancements/webdisplays/screen.json b/src/main/resources/assets/webdisplays/advancements/webdisplays/screen.json index a9969ea..20149a2 100644 --- a/src/main/resources/assets/webdisplays/advancements/webdisplays/screen.json +++ b/src/main/resources/assets/webdisplays/advancements/webdisplays/screen.json @@ -23,7 +23,8 @@ "webdisplays:redctrl1", "webdisplays:redctrl2", "webdisplays:stonekey", - "webdisplays:keyboard" + "webdisplays:keyboard", + "webdisplays:server" ] } } diff --git a/src/main/resources/assets/webdisplays/lang/en_us.lang b/src/main/resources/assets/webdisplays/lang/en_us.lang index 4fc39bc..d291e9d 100644 --- a/src/main/resources/assets/webdisplays/lang/en_us.lang +++ b/src/main/resources/assets/webdisplays/lang/en_us.lang @@ -122,10 +122,14 @@ webdisplays.server.timeout=Query timed out. Check logs. webdisplays.server.ownername=Owner name: %s webdisplays.server.owneruuid=Owner UUID: webdisplays.server.quota=%s/%s used -webdisplays.server.urlarg=Missing file name argument +webdisplays.server.fnamearg=Missing file name argument webdisplays.server.nameerr=Invalid file name webdisplays.server.urlcopied=Copied URL to clipboard. webdisplays.server.notfound=File not found +webdisplays.server.upload.uploading=Uploading... +webdisplays.server.upload.done=Done +webdisplays.server.upload.exists=Error: File exists +webdisplays.server.upload.quota=Error: File size exceeds quota webdisplays.server.help.help=Displays this text webdisplays.server.help.clear=Clears the screen webdisplays.server.help.exit=Leaves this console @@ -135,3 +139,4 @@ webdisplays.server.help.quota=Displays the storage quota webdisplays.server.help.ls=Lists the files on this server webdisplays.server.help.url=Copies a file URL into your clipboard webdisplays.server.help.upload=Opens the upload wizard +webdisplays.server.help.rm=Deletes a file diff --git a/src/main/resources/assets/webdisplays/recipes/ocinterface.json b/src/main/resources/assets/webdisplays/recipes/ocinterface.json index e90d1de..4281eb6 100644 --- a/src/main/resources/assets/webdisplays/recipes/ocinterface.json +++ b/src/main/resources/assets/webdisplays/recipes/ocinterface.json @@ -1,17 +1,16 @@ { "type": "minecraft:crafting_shaped", "pattern": [ - "GGG", - "GSG", - "GGG" + "PG" ], "key": { "G": { "item": "minecraft:dye", "data": 8 }, - "S": { - "item": "webdisplays:screen" + "P": { + "item": "webdisplays:craftcomp", + "data": 2 } }, "result": { diff --git a/src/main/resources/assets/webdisplays/recipes/server.json b/src/main/resources/assets/webdisplays/recipes/server.json new file mode 100644 index 0000000..e837004 --- /dev/null +++ b/src/main/resources/assets/webdisplays/recipes/server.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "PG" + ], + "key": { + "G": { + "item": "minecraft:dye", + "data": 2 + }, + "P": { + "item": "webdisplays:craftcomp", + "data": 2 + } + }, + "result": { + "item": "webdisplays:peripheral", + "data": 11 + } +}