diff --git a/README.md b/README.md index 4737db3f..09ac7536 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ mvn package ## Version history - - 1.11 - spawnEntity, setDirection, setRotation, setPitch + - 1.11 - spawnEntity, setSign, setDirection, setRotation, setPitch - 1.10.1 - bug fixes - 1.10 - left, right, both hit clicks added to config.yml & fixed minor hit events bug - 1.9.1 - minor change to improve connection reset diff --git a/src/main/java/net/zhuoweizhang/raspberryjuice/RemoteSession.java b/src/main/java/net/zhuoweizhang/raspberryjuice/RemoteSession.java index 5c0acbbe..f7af3fc8 100644 --- a/src/main/java/net/zhuoweizhang/raspberryjuice/RemoteSession.java +++ b/src/main/java/net/zhuoweizhang/raspberryjuice/RemoteSession.java @@ -1,6 +1,9 @@ package net.zhuoweizhang.raspberryjuice; import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.*; import java.util.*; @@ -8,6 +11,15 @@ import org.bukkit.entity.*; import org.bukkit.block.*; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.BookMeta.Generation; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.util.Vector; @@ -485,11 +497,25 @@ protected void handleCommand(String c, String[] args) { if ( thisBlock.getState() instanceof Sign ) { Sign sign = (Sign) thisBlock.getState(); for ( int i = 5; i-5 < 4 && i < args.length; i++) { - sign.setLine(i-5, args[i]); + sign.setLine(i-5, unescape(args[i])); } sign.update(); } + // world.addBookToChest + } else if (c.equals("world.addBookToChest")) { + Location loc = parseRelativeBlockLocation(args[0], args[1], args[2]); + Block thisBlock = world.getBlockAt(loc); + BlockState thisBlockState = thisBlock.getState(); + if ( thisBlockState instanceof InventoryHolder) { + InventoryHolder chest = (InventoryHolder) thisBlockState; + ItemStack book = createBookFromJson(unescape(args[3])); + chest.getInventory().addItem(book); + } else { + plugin.getLogger().info("addBook needs location of chest or other InventoryHolder"); + send("Fail"); + } + // world.spawnEntity } else if (c.equals("world.spawnEntity")) { Location loc = parseRelativeBlockLocation(args[0], args[1], args[2]); @@ -522,6 +548,17 @@ protected void handleCommand(String c, String[] args) { } } + + public String unescape(String s) { + if ( s == null ) return null; + s = s.replace(" ", "\n"); + s = s.replace("(", "("); + s = s.replace(")", ")"); + s = s.replace(",", ","); + s = s.replace("§", "§"); + s = s.replace("&", "&"); + return s; + } // create a cuboid of lots of blocks private void setCuboid(Location pos1, Location pos2, int blockType, byte data) { @@ -777,5 +814,199 @@ public static int blockFaceToNotch(BlockFace face) { return 7; // Good as anything here, but technically invalid } } + + /** + * Creates a WRITTEN_BOOK from JSON string including interactivity such as clicks and hovers. + * JSON format same as used by "/give" command without need to escape double quotes + * + * Inspired by https://github.com/upperlevel/spigot-book-api/blob/master/src/main/java/xyz/upperlevel/spigot/book/NmsBookHelper.java + * + * @author Tim Cummings + * @param json - JSON string used to define a book + * @return the book as an ItemStack + */ + public ItemStack createBookFromJson(String json) { + BookMeta meta = (BookMeta) Bukkit.getItemFactory().getItemMeta(Material.WRITTEN_BOOK); + JsonObject pyBook = new JsonParser().parse(json).getAsJsonObject(); + JsonElement pyTitle = pyBook.get("title"); + JsonElement pyAuthor = pyBook.get("author"); + JsonElement pyPages = pyBook.get("pages"); + JsonElement pyDisplay = pyBook.get("display"); + JsonElement pyGeneration = pyBook.get("generation"); + if (pyTitle != null) { + try { + meta.setTitle(pyTitle.getAsString()); + } catch (ClassCastException e) { + plugin.getLogger().info("Book title can't be got as string because it is not JsonPrimitive. Its JSON is " + pyTitle.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book title can't be got as string because it is a multiple element array. Its JSON is " + pyTitle.toString()); + e.printStackTrace(); + } + } + if (pyAuthor != null) { + try { + meta.setAuthor(pyAuthor.getAsString()); + } catch (ClassCastException e) { + plugin.getLogger().info("Book author can't be got as string because it is not JsonPrimitive. Its JSON is " + pyAuthor.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book author can't be got as string because it is a multiple element array. Its JSON is " + pyAuthor.toString()); + e.printStackTrace(); + } + } + if (pyDisplay != null) { + JsonElement pyDisplayName = null; + JsonElement pyDisplayLore = null; + if ( pyDisplay.isJsonObject() ) { + pyDisplayName = pyDisplay.getAsJsonObject().get("Name"); + pyDisplayLore = pyDisplay.getAsJsonObject().get("Lore"); + } + if (pyDisplayName != null) { + try { + meta.setDisplayName(pyDisplayName.getAsString()); + } catch (ClassCastException e) { + plugin.getLogger().info("Book display name can't be got as string because it is not JsonPrimitive. Its JSON is " + pyDisplayName.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book display name can't be got as string because it is a multiple element array. Its JSON is " + pyDisplayName.toString()); + e.printStackTrace(); + } + } + if (pyDisplayLore != null) { + List listLore = new ArrayList(); + if (pyDisplayLore.isJsonArray()) { + for (JsonElement je : pyDisplayLore.getAsJsonArray()) { + try { + listLore.add(je.getAsString()); + } catch (ClassCastException e) { + plugin.getLogger().info("Book display lore item can't be got as string because it is not JsonPrimitive. Its JSON is " + je.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book display lore item can't be got as string because it is a multiple element array. Its JSON is " + je.toString()); + e.printStackTrace(); + } + } + } else { + try { + listLore.add(pyDisplayLore.getAsString()); + } catch (ClassCastException e) { + plugin.getLogger().info("Book display lore can't be got as string because it is not JsonPrimitive. Really it should be JsonArray but if not we try this. Its JSON is " + pyDisplayLore.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book display lore can't be got as string because it is a multiple element array. This should never happen because we have already checked it is not a JsonArray. Its JSON is " + pyDisplayLore.toString()); + e.printStackTrace(); + } + } + } + } + if (pyGeneration != null) { + try { + int g = pyGeneration.getAsInt(); + Generation[] ga = Generation.values(); + if ( g >= 0 && g < ga.length ) { + meta.setGeneration(ga[g]); + } + } catch (ClassCastException e) { + plugin.getLogger().info("Book generation item can't be got as int because it is not JsonPrimitive of int. Its JSON is " + pyGeneration.toString()); + e.printStackTrace(); + } catch (IllegalStateException e) { + plugin.getLogger().info("Book generation item can't be got as int because it is a multiple element array rather than an int. Its JSON is " + pyGeneration.toString()); + e.printStackTrace(); + } + } + String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; + Class craftMetaBookClass = null; + Field craftMetaBookField = null; + String strCraftMetaBook = "org.bukkit.craftbukkit." + version + ".inventory.CraftMetaBook"; + try { + craftMetaBookClass = Class.forName(strCraftMetaBook); + } catch (ClassNotFoundException e) { + plugin.getLogger().warning("Can't get class " + strCraftMetaBook + " required to get pages of book that we want to modify"); + e.printStackTrace(); + } + if (craftMetaBookClass != null ) { + try { + craftMetaBookField = craftMetaBookClass.getDeclaredField("pages"); + craftMetaBookField.setAccessible(true); + } catch (NoSuchFieldException e) { + plugin.getLogger().info("Field 'pages' missing from class " + strCraftMetaBook + " required to get pages of book we want to modify"); + e.printStackTrace(); + } catch (SecurityException se) { + plugin.getLogger().warning("Security exception getting field 'pages' from class " + strCraftMetaBook + " required to get pages of book we want to modify"); + se.printStackTrace(); + } + } + Class chatSerializer = null; + String strChatSerializer1 = "net.minecraft.server." + version + ".IChatBaseComponent$ChatSerializer"; + String strChatSerializer2 = "net.minecraft.server." + version + ".ChatSerializer"; + String strChatSerializer = null; + try { + chatSerializer = Class.forName(strChatSerializer1); + strChatSerializer = strChatSerializer1; + } catch(ClassNotFoundException e) { + plugin.getLogger().info("Can't find class " + strChatSerializer1 + ". Will try " + strChatSerializer2); + e.printStackTrace(); + } + if ( chatSerializer == null ) { + try { + chatSerializer = Class.forName(strChatSerializer2); + strChatSerializer = strChatSerializer2; + } catch(ClassNotFoundException e) { + plugin.getLogger().warning("Can't find classes " + strChatSerializer1 + " or " + strChatSerializer2 + " needed to convert JSON to formatted interactive text in books"); + e.printStackTrace(); + } + } + Method chatSerializerA = null; + if ( chatSerializer != null ) { + try { + chatSerializerA = chatSerializer.getDeclaredMethod("a", String.class); + } catch (NoSuchMethodException e) { + plugin.getLogger().warning("Class " + strChatSerializer + " does not have method a() required to convert JSON to formatted interactive text in books"); + e.printStackTrace(); + } catch (SecurityException se) { + plugin.getLogger().warning("Security exception getting declared method a() from " + strChatSerializer); + se.printStackTrace(); + } + } + List pages = null; + //get the pages if required for json formatting + if (craftMetaBookField != null ) { + try { + @SuppressWarnings("unchecked") + List lo = (List) craftMetaBookField.get(meta); + pages = lo; + } catch (ReflectiveOperationException ex) { + plugin.getLogger().warning("Reflection exception getting pages from book using " + strCraftMetaBook + ".pages"); + ex.printStackTrace(); + } + } + if (pyPages.isJsonArray()) { + for (JsonElement jePage : pyPages.getAsJsonArray()) { + String page = jePage.toString(); + //plugin.getLogger().info(page); + if (chatSerializerA != null && pages != null) { + try { + pages.add(chatSerializerA.invoke(null, page)); + } catch (IllegalAccessException e) { + plugin.getLogger().warning("IllegalAccessException invoking method " + strChatSerializer + ".a() using reflection"); + e.printStackTrace(); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("IllegalArgumentException invoking method " + strChatSerializer + ".a() using reflection"); + e.printStackTrace(); + } catch (InvocationTargetException e) { + plugin.getLogger().warning("InvocationTargetException invoking method " + strChatSerializer + ".a() using reflection"); + e.printStackTrace(); + } + } else { + //something wrong with reflection methods so just add raw text to book + meta.addPage(page); + } + } + } + ItemStack book = new ItemStack(Material.WRITTEN_BOOK); + book.setItemMeta(meta); + return book; + } } diff --git a/src/main/resources/mcpi/api/python/modded/mcpi/book.py b/src/main/resources/mcpi/api/python/modded/mcpi/book.py new file mode 100644 index 00000000..2b50e1f0 --- /dev/null +++ b/src/main/resources/mcpi/api/python/modded/mcpi/book.py @@ -0,0 +1,110 @@ +import json + +class Book: + '''Minecraft PI Book description. Can be sent to Minecraft.addBookToChest(x,y,z,book) + + Book maintains a dictionary which describes the book and then converts it to a + json string when sent to the minecraft server. + Create a book from a dictionary + + b = mcpi.book.Book({"title":"Python", "author":"RaspberryJuice"}) + + or from a JSON string + + b = mcpi.book.Book.fromJson('{"title":"Python", "author":"RaspberryJuice"}') + + Pages can be added at creation time or added later one page at a time from a string + + b.addPageOfText("Some text") + + or from an array of dictionaries + + b.addPage('[{"text":"Some text"}]') + + Display parameters can be set at creation time or later from a dictionary + + b.setDisplay({"Name":"My display name"}) + + @author: Tim Cummings https://www.triptera.com.au/wordpress/''' + + def __init__(self, book): + """creates a book from dictionary. All keys in dictionary are optional but values should be correct type + + Example: + b = mcpi.book.Book({"title":"Python", "author":"RaspberryJuice", "pages":[]})""" + self.book = book + + def __eq__(self, other): + return ((self.book) == (other.book)) + + def __ne__(self, other): + return ((self.book) != (other.book)) + + def __lt__(self, other): + return ((self.book) < (other.book)) + + def __le__(self, other): + return ((self.book) <= (other.book)) + + def __gt__(self, other): + return ((self.book) > (other.book)) + + def __ge__(self, other): + return ((self.book) >= (other.book)) + + def __iter__(self): + '''Allows a Book to be used in flatten()''' + return iter((json.dumps(self.book),)) + + def __repr__(self): + return "Book.fromJson('{}')".format(json.dumps(self.book)) + + def __str__(self): + return json.dumps(self.book) + + @staticmethod + def fromJson(jsonstr): + """creates book from JSON string. All attributes are optional but should be correct type + + Example: + b = mcpi.book.Book.fromJson('{"title":"Json book","author":"RaspberryJuice","generation":0,"pages":[]}')""" + return Book(json.loads(jsonstr)) + + def addPageOfText(self,text): + """adds a new page of plain unformatted text to book + + New page is added after all existing pages + Example: + book.addPageOfText("My text on the page.\n\nNewlines can be added. Can't have formatting (color, bold, italic. etc) or interactivity (clicks or hovers)")""" + self.addPage([{"text":text}]) + + def addPage(self, page): + """adds a new page using an array of dictionaries describing all the text on the new page + + New page is added after all existing pages + Example: + addPage([ + {"text":'New "(page)"\nThis text is black ', "color":"reset"}, + {"text":"and this text is red, and bold.\n\n", "color":"red", "bold":True}, + {"text":"Hover or click the following\n"}, + {"text":"\nRunning a command\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"runs command to set daytime"}, + "clickEvent":{"action":"run_command", "value":"/time set day"}}, + {"text":"\nOpening a URL\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"opens url to RaspberryJuice"}, + "clickEvent":{"action":"open_url","value":"https://github.com/zhuowei/RaspberryJuice"}}, + {"text":"\nGoing to a page", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"goes to page 1"}, + "clickEvent":{"action":"change_page","value":1}} + ])""" + if 'pages' not in self.book: + self.book['pages'] = [] + self.book['pages'].append(page) + + def setDisplay(self, display): + """sets display name and lore and any other display parameters from a dictionary + + Replaces any previously set display + Example: + b.setDisplay({"Name":"My display string", "Lore":["An array of strings","describing lore"]})""" + self.book['display'] = display diff --git a/src/main/resources/mcpi/api/python/modded/mcpi/minecraft.py b/src/main/resources/mcpi/api/python/modded/mcpi/minecraft.py index bffb491d..b70db4e5 100644 --- a/src/main/resources/mcpi/api/python/modded/mcpi/minecraft.py +++ b/src/main/resources/mcpi/api/python/modded/mcpi/minecraft.py @@ -4,7 +4,7 @@ from .entity import Entity from .block import Block import math -from .util import flatten +from .util import flatten, escape """ Minecraft PI low level api v0.1_1 @@ -208,8 +208,22 @@ def setSign(self, *args): for arg in flatten(args): flatargs.append(arg) for flatarg in flatargs[5:]: - lines.append(flatarg.replace(",",";").replace(")","]").replace("(","[")) + lines.append(escape(flatarg)) self.conn.send(b"world.setSign",intFloor(flatargs[0:5]) + lines) + + def addBookToChest(self, *args): + """Add a book to a chest (x,y,z,book) + + The location x,y,z must contain a chest or other Inventory Holder + book is a JSON string or mcpi.book.Book object describing the book + @author: Tim Cummings https://www.triptera.com.au/wordpress/""" + bookmeta = [] + flatargs = [] + for arg in flatten(args): + flatargs.append(arg) + for flatarg in flatargs[3:]: + bookmeta.append(escape(flatarg)) + self.conn.send(b"world.addBookToChest",intFloor(flatargs[0:3]) + bookmeta) def spawnEntity(self, *args): """Spawn entity (x,y,z,id,[data])""" diff --git a/src/main/resources/mcpi/api/python/modded/mcpi/util.py b/src/main/resources/mcpi/api/python/modded/mcpi/util.py index 9791072e..e7d016f9 100644 --- a/src/main/resources/mcpi/api/python/modded/mcpi/util.py +++ b/src/main/resources/mcpi/api/python/modded/mcpi/util.py @@ -16,3 +16,15 @@ def _misc_to_bytes(m): See `Connection.send` for more details. """ return str(m).encode("cp437") + +def escape(s): + """Escape content of strings which will break the api using html entity type escaping""" + s = s.replace("&","&") + s = s.replace("\r\n"," ") + s = s.replace("\n"," ") + s = s.replace("\r"," ") + s = s.replace("(","(") + s = s.replace(")",")") + s = s.replace(",",",") + s = s.replace("§","§") + return s diff --git a/src/main/resources/test/Test.py b/src/main/resources/test/Test.py index 87598f83..fd97b7cf 100644 --- a/src/main/resources/test/Test.py +++ b/src/main/resources/test/Test.py @@ -7,16 +7,25 @@ import original.mcpi.block as block import modded.mcpi.block as blockmodded import modded.mcpi.entity as entitymodded +import modded.mcpi.book as bookmodded import time import math -def runBlockTests(mc): +def clearTestArea(mc, xtest=0, ytest=100, ztest=0): + "clear the area in segments, otherwise it breaks the server, glowstone shows progress if area already clear" + mc.setBlocks(xtest,ytest-1,ztest,xtest+150,ytest-1,ztest,blockmodded.GLOWSTONE_BLOCK) + for x_inc in range(151): + mc.setBlocks(xtest+x_inc,ytest-2,ztest,xtest+x_inc,ytest+25,ztest+100,blockmodded.AIR) + time.sleep(1) + +def runBlockTests(mc, xtest=0, ytest=100, ztest=0): """runBlockTests - tests creation of all blocks for all data values known to RaspberryJuice A sign is placed next to the created block so user can view in Minecraft whether block created correctly or not + xtest, ytest, ztest are coordinates where tests should start. Known issues: - id for NETHER_REACTOR_CORE and GLOWING_OBSIDIAN wrong - - some LEAVES missing but because they decay by the time user sees them + - some LEAVES missing because they decay by the time user sees them - this test doesn't try activation of TNT Author: Tim Cummings https://www.triptera.com.au/wordpress/ @@ -53,9 +62,6 @@ def runBlockTests(mc): mushrooms=["MUSHROOM_BROWN","MUSHROOM_RED"] # location for platform showing all block types - xtest = 0 - ytest = 50 - ztest = 0 mc.postToChat("runBlockTests(): Creating test blocks at x=" + str(xtest) + " y=" + str(ytest) + " z=" + str(ztest)) # create set of all block ids to ensure they all get tested # note some blocks have different names but same ids so they only have to be tested once per id @@ -81,22 +87,26 @@ def runBlockTests(mc): sign=blockmodded.SIGN_STANDING.withData(12) signid=sign.id - x=xtest - y=ytest-1 - z=ztest - mc.setBlocks(x,y,z,x+100,y,z+100,blockmodded.STONE) - time.sleep(1) - #clear the area in segments, otherwise it breaks the server - #clearing area - #mc.setBlocks(x,y+1,z,x+100,y+50,z+100,blockmodded.AIR) - for y_inc in range(1, 10): - mc.setBlocks(x,y+y_inc,z,x+100,y+y_inc,z+100,blockmodded.AIR) - time.sleep(2) mc.player.setTilePos(xtest, ytest, ztest) + mc.player.setRotation(-45) + + #setup a stone area in segments as a platform for the block and book tests with moving glowstone to measure progress" + for x_inc in range(5): + mc.setBlocks(xtest+x_inc*10,ytest-1,ztest,xtest+(x_inc+1)*10,ytest-1,ztest+100,blockmodded.STONE) + mc.setBlock(xtest+x_inc*10,ytest,ztest,blockmodded.GLOWSTONE_BLOCK) + time.sleep(1) + mc.setBlock(xtest+x_inc*10,ytest,ztest,blockmodded.AIR) + for x_inc in range(5): + mc.setBlocks(xtest+50+x_inc*20,ytest-1,ztest,xtest+50+(x_inc+1)*20,ytest-1,ztest+50,blockmodded.STONE) + mc.setBlock(xtest+50+x_inc*20,ytest,ztest,blockmodded.GLOWSTONE_BLOCK) + time.sleep(1) + mc.setBlock(xtest+50+x_inc*20,ytest,ztest,blockmodded.AIR) + time.sleep(1) x=xtest+10 y=ytest z=ztest+10 + mc.setBlock(xtest+10,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in solids + gases + flats + fences: b = getattr(blockmodded,key) z += 1 @@ -108,6 +118,8 @@ def runBlockTests(mc): time.sleep(1) x=xtest+20 z=ztest+10 + mc.setBlock(xtest+10,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+20,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in trees: for data in range(16): b = getattr(blockmodded,key).withData(data) @@ -181,6 +193,8 @@ def runBlockTests(mc): time.sleep(1) x=xtest+30 z=ztest+10 + mc.setBlock(xtest+20,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+30,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in coloureds: for data in range(16): b = getattr(blockmodded,key).withData(data) @@ -232,6 +246,8 @@ def runBlockTests(mc): time.sleep(1) x=xtest+40 z=ztest+10 + mc.setBlock(xtest+30,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+40,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in liquids: z += 1 mc.setBlocks(x-1,y,z,x+1,y,z+10,blockmodded.STONE) @@ -259,6 +275,8 @@ def runBlockTests(mc): time.sleep(1) x=xtest+50 z=ztest+10 + mc.setBlock(xtest+40,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+50,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in slabs: for data in range(8): b = getattr(blockmodded,key).withData(data) @@ -272,6 +290,8 @@ def runBlockTests(mc): x=xtest+60 y=ytest z=ztest+10 + mc.setBlock(xtest+50,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+60,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) wallsignid=blockmodded.SIGN_WALL.id for key in wallmounts: b = getattr(blockmodded,key) @@ -322,6 +342,8 @@ def runBlockTests(mc): x=xtest+70 y=ytest z=ztest+10 + mc.setBlock(xtest+60,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+70,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in doors: b = getattr(blockmodded,key) mc.setBlocks(x,y,z,x+3,y+2,z+3,signmount) @@ -387,12 +409,15 @@ def runBlockTests(mc): untested.discard(b.id) time.sleep(1) - x=xtest+70 + x=xtest+80 y=ytest - z=ztest+20 + z=ztest+10 + mc.setBlock(xtest+70,ytest,ztest+5,blockmodded.AIR) + mc.setBlock(xtest+80,ytest,ztest+5,blockmodded.GLOWSTONE_BLOCK) for key in stairs: b = getattr(blockmodded,key) - mc.setBlocks(x+1,y,z-1,x+3,y+11,z-3,signmount) + mc.setBlocks(x ,y,z ,x+4,y+15,z-4,blockmodded.AIR) + mc.setBlocks(x+1,y,z-1,x+3,y+15,z-3,signmount) mc.setBlock(x+1,y ,z ,b.id,0) mc.setBlock(x+2,y+1,z ,b.id,0) mc.setBlock(x+3,y+2,z ,b.id,0) @@ -409,29 +434,39 @@ def runBlockTests(mc): mc.setBlock(x ,y+10,z-2,b.id,2) mc.setBlock(x ,y+11,z-1,b.id,2) mc.setBlock(x ,y+11,z ,signmount) - mc.setBlock(x+1,y-1,z ,b.id,5) - mc.setBlock(x+2,y ,z ,b.id,5) - mc.setBlock(x+3,y+1,z ,b.id,5) - mc.setBlock(x+4,y+2,z-1,b.id,6) - mc.setBlock(x+4,y+3,z-2,b.id,6) - mc.setBlock(x+4,y+4,z-3,b.id,6) - mc.setBlock(x+3,y+5,z-4,b.id,4) - mc.setBlock(x+2,y+6,z-4,b.id,4) - mc.setBlock(x+1,y+7,z-4,b.id,4) - mc.setBlock(x ,y+8,z-3,b.id,7) - mc.setBlock(x ,y+9,z-2,b.id,7) - mc.setBlock(x ,y+10,z-1,b.id,7) + mc.setBlock(x+1,y+4,z ,b.id,5) + mc.setBlock(x+2,y+5,z ,b.id,5) + mc.setBlock(x+3,y+6,z ,b.id,5) + mc.setBlock(x+4,y+7,z ,signmount) + mc.setBlock(x+4,y+7,z-1,b.id,6) + mc.setBlock(x+4,y+8,z-2,b.id,6) + mc.setBlock(x+4,y+9,z-3,b.id,6) + mc.setBlock(x+4,y+9,z-4,signmount) + mc.setBlock(x+3,y+10,z-4,b.id,4) + mc.setBlock(x+2,y+11,z-4,b.id,4) + mc.setBlock(x+1,y+12,z-4,b.id,4) + mc.setBlock(x ,y+12,z-4,signmount) + mc.setBlock(x ,y+13,z-3,b.id,7) + mc.setBlock(x ,y+14,z-2,b.id,7) + mc.setBlock(x ,y+15,z-1,b.id,7) + mc.setBlock(x ,y+15,z ,signmount) mc.setSign (x+2,y+2 ,z ,wallsignid,3,key,"id=" + str(b.id),"data=0") - mc.setSign (x+3,y ,z ,wallsignid,3,key,"id=" + str(b.id),"data=5") + mc.setSign (x+3,y+5 ,z ,wallsignid,3,key,"id=" + str(b.id),"data=5") mc.setSign (x+4,y+5 ,z-2,wallsignid,5,key,"id=" + str(b.id),"data=3") - mc.setSign (x+4,y+3 ,z-3,wallsignid,5,key,"id=" + str(b.id),"data=6") + mc.setSign (x+4,y+8 ,z-3,wallsignid,5,key,"id=" + str(b.id),"data=6") mc.setSign (x+2,y+8 ,z-4,wallsignid,2,key,"id=" + str(b.id),"data=1") - mc.setSign (x+1,y+6 ,z-4,wallsignid,2,key,"id=" + str(b.id),"data=4") + mc.setSign (x+1,y+11,z-4,wallsignid,2,key,"id=" + str(b.id),"data=4") mc.setSign (x ,y+11,z-2,wallsignid,4,key,"id=" + str(b.id),"data=2") - mc.setSign (x ,y+9 ,z-1,wallsignid,4,key,"id=" + str(b.id),"data=7") - y+=12 + mc.setSign (x ,y+14,z-1,wallsignid,4,key,"id=" + str(b.id),"data=7") + time.sleep(0.1) + z+=8 + if z > ztest+40: + z = ztest+10 + x += 10 untested.discard(b.id) + mc.setBlock(xtest+80,ytest,ztest+5,blockmodded.AIR) + #Display list of all blocks which did not get tested for id in untested: untest="Untested block " + str(id) @@ -440,7 +475,7 @@ def runBlockTests(mc): mc.postToChat(untest) mc.postToChat("runBlockTests() complete") -def runEntityTests(mc): +def runEntityTests(mc, xtest=50, ytest=100, ztest=50): """runEntityTests - tests creation of all entities known to RaspberryJuice A sign is placed next to the created entity so user can view in Minecraft whether block created correctly or not @@ -464,10 +499,7 @@ def runEntityTests(mc): giants=["ELDER_GUARDIAN","GUARDIAN","GIANT","ENDER_DRAGON","GHAST"] cavers=["MAGMA_CUBE","BAT","PARROT","CHICKEN","STRAY","SKELETON","SPIDER","ZOMBIE","SLIME","CAVE_SPIDER","PIG_ZOMBIE","ENDERMAN","SNOWMAN","SILVERFISH","ILLUSIONER"] bosses=["WITHER"] - # location for platform showing all block types - xtest = 50 - ytest = 50 - ztest = 50 + # location for platform showing all entities air=blockmodded.AIR wall=blockmodded.GLASS roof=blockmodded.STONE @@ -480,15 +512,12 @@ def runEntityTests(mc): rail=blockmodded.RAIL wallsignid=blockmodded.SIGN_WALL.id mc.postToChat("runEntityTests(): Creating test entities at x=" + str(xtest) + " y=" + str(ytest) + " z=" + str(ztest)) - #mc.setBlocks(xtest,ytest-1,ztest,xtest+100,ytest+50,ztest+100,air) - #clear the area in segments, otherwise it breaks the server - #clearing area - for y_inc in range(0, 10): - mc.setBlocks(xtest,ytest+y_inc,ztest,xtest+100,ytest+y_inc,ztest+100,air) - time.sleep(2) - - mc.setBlocks(xtest,ytest-1,ztest-1,xtest+100,ytest-1,ztest+100,floor) + mc.setBlocks(xtest,ytest-1,ztest-1,xtest+100,ytest-1,ztest+50,floor) + # Only thing that really has to be cleared is mounting wall for hangers. + # Test will crash if previous hangers still exist when trying to mount new ones + # Clear wall now before dancing villager so minecraft client has time to clear before making new one. + mc.setBlocks(xtest+2,ytest,ztest-1,xtest+2,ytest+2,ztest+1+len(hangers)*3,air) mc.player.setTilePos(xtest, ytest, ztest) mc.postToChat("Dancing villager") @@ -499,7 +528,7 @@ def runEntityTests(mc): id=mc.spawnEntity(x,y,z,entitymodded.VILLAGER) theta = 0 while theta <= 2 * math.pi: - time.sleep(1) + time.sleep(0.1) theta += 0.1 x = xtest + math.sin(theta) * r z = ztest + math.cos(theta) * r @@ -531,16 +560,6 @@ def runEntityTests(mc): x=xtest y=ytest z=ztest - for key in items: - z += 2 - if z > 98: - z = ztest - x += 10 - e = getattr(entitymodded,key) - mc.setBlock(x+2,y,z,signmount) - mc.setSign(x+2,y+1,z,sign,key,"id=" + str(e.id)) - mc.spawnEntity(x,y,z,e) - untested.discard(e.id) for key in hangers: z += 3 if z > 97: @@ -551,6 +570,16 @@ def runEntityTests(mc): mc.setSign(x+1,y,z,wallsignid,4,key,"id=" + str(e.id)) mc.spawnEntity(x+1,y+2,z,e) untested.discard(e.id) + for key in items: + z += 2 + if z > 98: + z = ztest + x += 10 + e = getattr(entitymodded,key) + mc.setBlock(x+2,y,z,signmount) + mc.setSign(x+2,y+1,z,sign,key,"id=" + str(e.id)) + mc.spawnEntity(x,y,z,e) + untested.discard(e.id) z = ztest - 4 x += 10 time.sleep(1) @@ -613,17 +642,35 @@ def runEntityTests(mc): mc.setSign(x-1,y,z+2,sign,key,"id=" + str(e.id)) mc.spawnEntity(x+2,y,z+2,e) untested.discard(e.id) + + x+=10 + z=ztest + wallheight=10 + time.sleep(1) + for key in sinks: + e = getattr(entitymodded,key) + mc.setBlocks(x,y,z,x+10,y+wallheight,z+10,wall) + mc.setBlocks(x+1,y,z+1,x+9,y+wallheight,z+9,blockmodded.WATER_STATIONARY) + mc.setBlock(x-1,y,z+1,torch) + mc.setSign(x-1,y,z+2,sign,key,"id=" + str(e.id)) + mc.spawnEntity(x+5,y+5,z+5,e) + untested.discard(e.id) + z += 10 + if z > 90: + z = ztest + x += 15 x=xtest y=ytest+10 z=ztest + wallheight=13 time.sleep(1) for key in giants: e = getattr(entitymodded,key) - mc.setBlocks(x,y,z,x+20,y+20,z+20,wall) - mc.setBlocks(x,y+21,z,x+20,y+21,z+20,roof) + mc.setBlocks(x,y,z,x+20,y+wallheight,z+20,wall) + mc.setBlocks(x,y+wallheight+1,z,x+20,y+wallheight+1,z+20,roof) mc.setBlocks(x-5,y-1,z-1,x+20,y-1,z+21,floor) - mc.setBlocks(x+1,y,z+1,x+19,y+20,z+19,air) + mc.setBlocks(x+1,y,z+1,x+19,y+wallheight,z+19,air) mc.setSign(x-1,y,z+2,sign,key,"id=" + str(e.id)) mc.spawnEntity(x+10,y+5,z+10,e) untested.discard(e.id) @@ -634,15 +681,15 @@ def runEntityTests(mc): time.sleep(1) for key in bosses: e = getattr(entitymodded,key) - mc.setBlocks(x,y,z,x+20,y+20,z+20,blockmodded.BEDROCK) - mc.setBlocks(x,y+21,z,x+20,y+21,z+20,blockmodded.BEDROCK) + mc.setBlocks(x,y,z,x+20,y+wallheight,z+20,blockmodded.BEDROCK) + mc.setBlocks(x,y+wallheight+1,z,x+20,y+wallheight+1,z+20,blockmodded.BEDROCK) mc.setBlocks(x-5,y-1,z-1,x+20,y-1,z+21,blockmodded.BEDROCK) - mc.setBlocks(x+1,y,z+1,x+19,y+20,z+19,air) + mc.setBlocks(x+1,y,z+1,x+19,y+wallheight,z+19,air) mc.setBlocks(x+1,y,z+8,x+19,y,z+12,torch) - mc.setBlocks(x+1,y+10,z+2,x+1,y+15,z+18,torch.id,1) - mc.setBlocks(x+19,y+10,z+2,x+19,y+15,z+18,torch.id,2) - mc.setBlocks(x+1,y+10,z+19,x+19,y+15,z+19,torch.id,4) - mc.setBlocks(x+1,y+10,z+1,x+19,y+15,z+1,torch.id,3) + mc.setBlocks(x+1,y+7,z+2,x+1,y+10,z+18,torch.id,1) + mc.setBlocks(x+19,y+7,z+2,x+19,y+10,z+18,torch.id,2) + mc.setBlocks(x+1,y+7,z+19,x+19,y+10,z+19,torch.id,4) + mc.setBlocks(x+1,y+7,z+1,x+19,y+10,z+1,torch.id,3) mc.setBlocks(x,y,z+9,x+3,y+3,z+11,blockmodded.BEDROCK) mc.setBlocks(x+4,y,z+9,x+19,y+3,z+11,wall) mc.setBlocks(x,y,z+10,x+19,y+2,z+10,air) @@ -653,20 +700,6 @@ def runEntityTests(mc): if z > 80: z = ztest x += 25 - time.sleep(1) - for key in sinks: - e = getattr(entitymodded,key) - mc.setBlocks(x,y,z,x+20,y+20,z+20,wall) - mc.setBlocks(x,y+21,z,x+20,y+21,z+20,roof) - mc.setBlocks(x-5,y-1,z-1,x+20,y-1,z+21,floor) - mc.setBlocks(x+1,y,z+1,x+19,y+20,z+19,blockmodded.WATER_STATIONARY) - mc.setSign(x-1,y,z+2,sign,key,"id=" + str(e.id)) - mc.spawnEntity(x+10,y,z+10,e) - untested.discard(e.id) - z += 20 - if z > 80: - z = ztest - x += 25 #Display list of all entities which did not get tested for id in untested: @@ -678,6 +711,46 @@ def runEntityTests(mc): mc.postToChat("/kill @e[type=!player]") mc.postToChat("to remove test entities") +def runBookTests(mc, xtest=48, ytest=100, ztest=48): + """runBookTests - tests creation of book in a chest + + Author: Tim Cummings https://www.triptera.com.au/wordpress/ + """ + # location for chest containing book xtest, ytest, ztest + mc.postToChat("runBookTests(): Creating chest with book at x=" + str(xtest) + " y=" + str(ytest) + " z=" + str(ztest)) + b = bookmodded.Book({"title":"RaspberryJuice Test", "author":"Tim Cummings"}) + b.addPageOfText('This page should be unformatted text. ' + 'Please turn to page 2 to see formatted interactive text.\n\n' + 'Test escapes\n' + 'Comma ,\n' + 'Parentheses ()\n' + 'Double quotes " "\n' + "Single quotes ' '\n" + 'Section §\n' + 'Ampersand &') + page = [ + {"text":'This text is black ', "color":"reset"}, + {"text":"and this text is red and bold. ", "color":"red", "bold":True}, + {"text":"Hover or click the following:\n"}, + {"text":"\nRunning a command\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"runs command to set daytime"}, + "clickEvent":{"action":"run_command", "value":"/time set day"}}, + {"text":"\nOpening a URL\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"opens url to RaspberryJuice"}, + "clickEvent":{"action":"open_url","value":"https://github.com/zhuowei/RaspberryJuice"}}, + {"text":"\nGoing to a page\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"goes to page 1"}, + "clickEvent":{"action":"change_page","value":1}}, + {"text":"\nCleanup\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"kills all non player entities after entity test"}, + "clickEvent":{"action":"run_command", "value":"/kill @e[type=!player]"}} + ] + b.addPage(page) + b.setDisplay({"Name":"Test book", "Lore":["First line of lore","Second line of lore"]}) + mc.setBlock(xtest,ytest,ztest,blockmodded.CHEST) + mc.addBookToChest(xtest,ytest,ztest,b) + + def runTests(mc, library="Standard library", extended=False): @@ -728,8 +801,8 @@ def runTests(mc, library="Standard library", extended=False): #getBlocks if extended: - listOfBlocks = mc.getBlocks(pos.x,pos.y + 10,pos.z, - pos.x + 5, pos.y + 15, pos.z + 5) + listOfBlocks = list(mc.getBlocks(pos.x,pos.y + 10,pos.z, + pos.x + 5, pos.y + 15, pos.z + 5)) print(listOfBlocks) #getPlayerEntityIds @@ -775,20 +848,40 @@ def runTests(mc, library="Standard library", extended=False): mc.spawnEntity(tilePos.x + 2, tilePos.y + 2, tilePos.x + 2, entitymodded.CREEPER) mc.postToChat("Creeper spawned") - mc.postToChat("Post To Chat - Run full block and entity test Y/N?") + mc.postToChat("Post To Chat - a single letter to choose from following options") + mc.postToChat(" B=run full block and book tests") + mc.postToChat(" E=run full entity tests") + mc.postToChat(" A=run all block, book and entity tests") + mc.postToChat(" C=clear full test area") + mc.postToChat(" F=both clear area and run full tests") + mc.postToChat(" S=skip these tests") chatPosted = False - fullTests = False + blockTests = False + entityTests = False + clearArea = False while not chatPosted: time.sleep(1) chatPosts = mc.events.pollChatPosts() for chatPost in chatPosts: mc.postToChat("Echo " + chatPost.message) chatPosted = True - if chatPost.message == "Y": - fullTests = True + chatLetter = chatPost.message.strip().upper() + if len(chatLetter) > 0: chatLetter = chatLetter[0] + if chatLetter in ('F','B','A'): + blockTests = True + if chatLetter in ('F','E','A'): + entityTests = True + if chatLetter in ('C','F'): + clearArea = True - if fullTests: + if clearArea: + clearTestArea(mc) + + if blockTests: runBlockTests(mc) + runBookTests(mc) + + if entityTests: runEntityTests(mc) mc.postToChat("Tests complete for " + library) diff --git a/src/main/resources/test/modded/mcpi/book.py b/src/main/resources/test/modded/mcpi/book.py new file mode 100644 index 00000000..2b50e1f0 --- /dev/null +++ b/src/main/resources/test/modded/mcpi/book.py @@ -0,0 +1,110 @@ +import json + +class Book: + '''Minecraft PI Book description. Can be sent to Minecraft.addBookToChest(x,y,z,book) + + Book maintains a dictionary which describes the book and then converts it to a + json string when sent to the minecraft server. + Create a book from a dictionary + + b = mcpi.book.Book({"title":"Python", "author":"RaspberryJuice"}) + + or from a JSON string + + b = mcpi.book.Book.fromJson('{"title":"Python", "author":"RaspberryJuice"}') + + Pages can be added at creation time or added later one page at a time from a string + + b.addPageOfText("Some text") + + or from an array of dictionaries + + b.addPage('[{"text":"Some text"}]') + + Display parameters can be set at creation time or later from a dictionary + + b.setDisplay({"Name":"My display name"}) + + @author: Tim Cummings https://www.triptera.com.au/wordpress/''' + + def __init__(self, book): + """creates a book from dictionary. All keys in dictionary are optional but values should be correct type + + Example: + b = mcpi.book.Book({"title":"Python", "author":"RaspberryJuice", "pages":[]})""" + self.book = book + + def __eq__(self, other): + return ((self.book) == (other.book)) + + def __ne__(self, other): + return ((self.book) != (other.book)) + + def __lt__(self, other): + return ((self.book) < (other.book)) + + def __le__(self, other): + return ((self.book) <= (other.book)) + + def __gt__(self, other): + return ((self.book) > (other.book)) + + def __ge__(self, other): + return ((self.book) >= (other.book)) + + def __iter__(self): + '''Allows a Book to be used in flatten()''' + return iter((json.dumps(self.book),)) + + def __repr__(self): + return "Book.fromJson('{}')".format(json.dumps(self.book)) + + def __str__(self): + return json.dumps(self.book) + + @staticmethod + def fromJson(jsonstr): + """creates book from JSON string. All attributes are optional but should be correct type + + Example: + b = mcpi.book.Book.fromJson('{"title":"Json book","author":"RaspberryJuice","generation":0,"pages":[]}')""" + return Book(json.loads(jsonstr)) + + def addPageOfText(self,text): + """adds a new page of plain unformatted text to book + + New page is added after all existing pages + Example: + book.addPageOfText("My text on the page.\n\nNewlines can be added. Can't have formatting (color, bold, italic. etc) or interactivity (clicks or hovers)")""" + self.addPage([{"text":text}]) + + def addPage(self, page): + """adds a new page using an array of dictionaries describing all the text on the new page + + New page is added after all existing pages + Example: + addPage([ + {"text":'New "(page)"\nThis text is black ', "color":"reset"}, + {"text":"and this text is red, and bold.\n\n", "color":"red", "bold":True}, + {"text":"Hover or click the following\n"}, + {"text":"\nRunning a command\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"runs command to set daytime"}, + "clickEvent":{"action":"run_command", "value":"/time set day"}}, + {"text":"\nOpening a URL\n", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"opens url to RaspberryJuice"}, + "clickEvent":{"action":"open_url","value":"https://github.com/zhuowei/RaspberryJuice"}}, + {"text":"\nGoing to a page", "underlined":True, "color":"blue", + "hoverEvent":{"action":"show_text","value":"goes to page 1"}, + "clickEvent":{"action":"change_page","value":1}} + ])""" + if 'pages' not in self.book: + self.book['pages'] = [] + self.book['pages'].append(page) + + def setDisplay(self, display): + """sets display name and lore and any other display parameters from a dictionary + + Replaces any previously set display + Example: + b.setDisplay({"Name":"My display string", "Lore":["An array of strings","describing lore"]})""" + self.book['display'] = display diff --git a/src/main/resources/test/modded/mcpi/minecraft.py b/src/main/resources/test/modded/mcpi/minecraft.py index bffb491d..b70db4e5 100644 --- a/src/main/resources/test/modded/mcpi/minecraft.py +++ b/src/main/resources/test/modded/mcpi/minecraft.py @@ -4,7 +4,7 @@ from .entity import Entity from .block import Block import math -from .util import flatten +from .util import flatten, escape """ Minecraft PI low level api v0.1_1 @@ -208,8 +208,22 @@ def setSign(self, *args): for arg in flatten(args): flatargs.append(arg) for flatarg in flatargs[5:]: - lines.append(flatarg.replace(",",";").replace(")","]").replace("(","[")) + lines.append(escape(flatarg)) self.conn.send(b"world.setSign",intFloor(flatargs[0:5]) + lines) + + def addBookToChest(self, *args): + """Add a book to a chest (x,y,z,book) + + The location x,y,z must contain a chest or other Inventory Holder + book is a JSON string or mcpi.book.Book object describing the book + @author: Tim Cummings https://www.triptera.com.au/wordpress/""" + bookmeta = [] + flatargs = [] + for arg in flatten(args): + flatargs.append(arg) + for flatarg in flatargs[3:]: + bookmeta.append(escape(flatarg)) + self.conn.send(b"world.addBookToChest",intFloor(flatargs[0:3]) + bookmeta) def spawnEntity(self, *args): """Spawn entity (x,y,z,id,[data])""" diff --git a/src/main/resources/test/modded/mcpi/util.py b/src/main/resources/test/modded/mcpi/util.py index 9791072e..e7d016f9 100644 --- a/src/main/resources/test/modded/mcpi/util.py +++ b/src/main/resources/test/modded/mcpi/util.py @@ -16,3 +16,15 @@ def _misc_to_bytes(m): See `Connection.send` for more details. """ return str(m).encode("cp437") + +def escape(s): + """Escape content of strings which will break the api using html entity type escaping""" + s = s.replace("&","&") + s = s.replace("\r\n"," ") + s = s.replace("\n"," ") + s = s.replace("\r"," ") + s = s.replace("(","(") + s = s.replace(")",")") + s = s.replace(",",",") + s = s.replace("§","§") + return s