/* * Copyright (C) 2018 BARBOTIN Nicolas */ package net.montoyo.wd.client; import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.montoyo.mcef.api.IBrowser; import net.montoyo.mcef.api.IJSQueryCallback; import net.montoyo.wd.WebDisplays; import net.montoyo.wd.block.BlockScreen; import net.montoyo.wd.core.DefaultUpgrade; import net.montoyo.wd.core.IScreenQueryHandler; import net.montoyo.wd.core.IUpgrade; import net.montoyo.wd.core.JSServerRequest; import net.montoyo.wd.entity.TileEntityScreen; import net.montoyo.wd.net.Messages; import net.montoyo.wd.net.server.SMessageScreenCtrl; import net.montoyo.wd.utilities.*; import java.util.*; @OnlyIn(Dist.CLIENT) public final class JSQueryDispatcher { private static final class QueryData { private final IBrowser browser; private final String query; private final String args; private final IJSQueryCallback callback; private QueryData(IBrowser b, String q, String a, IJSQueryCallback cb) { browser = b; query = q; args = a; callback = cb; } } public static final class ServerQuery { private static int lastId = 0; private final TileEntityScreen tes; private final BlockSide side; private final IJSQueryCallback callback; private final int id; private ServerQuery(TileEntityScreen t, BlockSide s, IJSQueryCallback cb) { tes = t; side = s; callback = cb; id = lastId++; } public TileEntityScreen getTileEntity() { return tes; } public BlockSide getSide() { return side; } public TileEntityScreen.Screen getScreen() { return tes.getScreen(side); } public void success(String resp) { callback.success(resp); } public void error(int errId, String errStr) { callback.failure(errId, errStr); } } private final ClientProxy proxy; private final ArrayDeque queue = new ArrayDeque<>(); private final ClientProxy.ScreenSidePair lookupResult = new ClientProxy.ScreenSidePair(); private final HashMap handlers = new HashMap<>(); private final ArrayList serverQueries = new ArrayList<>(); private final Minecraft mc = Minecraft.getInstance(); public JSQueryDispatcher(ClientProxy proxy) { this.proxy = proxy; registerDefaults(); } public void enqueueQuery(IBrowser b, String q, String a, IJSQueryCallback cb) { synchronized(queue) { queue.offer(new QueryData(b, q, a, cb)); } } public void handleQueries() { while(true) { QueryData next; synchronized(queue) { next = queue.poll(); } if(next == null) break; if(proxy.findScreenFromBrowser(next.browser, lookupResult)) { Object[] args = (next.args == null) ? new Object[0] : parseArgs(next.args); if(args == null) next.callback.failure(400, "Malformed request parameters"); else { try { handlers.get(next.query).handleQuery(next.callback, lookupResult.tes, lookupResult.side, args); } catch(Throwable t) { Log.warningEx("Could not execute JS query %s(%s)", t, next.query, (next.args == null) ? "" : next.args); next.callback.failure(500, "Internal error"); } } } else next.callback.failure(403, "A screen is required"); } } public boolean canHandleQuery(String q) { return handlers.containsKey(q); } private static Object[] parseArgs(String args) { ArrayList array = new ArrayList<>(); int lastIdx = 0; boolean inString = false; boolean escape = false; boolean hadString = false; for(int i = 0; i < args.length(); i++) { char chr = args.charAt(i); if(inString) { if(escape) escape = false; else { if(chr == '\"') inString = false; else if(chr == '\\') escape = true; } } else if(chr == '\"') { if(hadString) return null; inString = true; hadString = true; } else if(chr == ',') { array.add(args.substring(lastIdx, i).trim()); lastIdx = i + 1; hadString = false; } } if(inString) return null; //Non terminated string array.add(args.substring(lastIdx).trim()); Object[] ret = new Object[array.size()]; for(int i = 0; i < ret.length; i++) { String str = array.get(i); if(str.isEmpty()) return null; //Nah... if(str.charAt(0) == '\"') //String ret[i] = str.substring(1, str.length() - 1); else { try { ret[i] = Double.parseDouble(str); } catch(NumberFormatException ex) { return null; } } } return ret; } public void register(String query, IScreenQueryHandler handler) { handlers.put(query.toLowerCase(), handler); } public ServerQuery fulfillQuery(int id) { int toRemove = -1; for(int i = 0; i < serverQueries.size(); i++) { ServerQuery sq = serverQueries.get(i); if(sq.id == id) { toRemove = i; break; } } if(toRemove < 0) return null; else return serverQueries.remove(toRemove); } private void makeServerQuery(TileEntityScreen tes, BlockSide side, IJSQueryCallback cb, JSServerRequest type, Object ... data) { ServerQuery ret = new ServerQuery(tes, side, cb); serverQueries.add(ret); Messages.INSTANCE.sendToServer(SMessageScreenCtrl.jsRequest(tes, side, ret.id, type, data)); } private void registerDefaults() { register("GetSize", (cb, tes, side, args) -> { Vector2i size = tes.getScreen(side).size; cb.success("{\"x\":" + size.x + ",\"y\":" + size.y + "}"); }); register("GetRedstoneAt", (cb, tes, side, args) -> { if(!tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_INPUT)) { cb.failure(403, "Missing upgrade"); return; } if(args.length == 2 && args[0] instanceof Double && args[1] instanceof Double) { TileEntityScreen.Screen scr = tes.getScreen(side); int x = ((Double) args[0]).intValue(); int y = ((Double) args[1]).intValue(); if(x < 0 || x >= scr.size.x || y < 0 || y >= scr.size.y) cb.failure(403, "Out of range"); else { BlockPos bpos = (new Vector3i(tes.getBlockPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); int level = tes.getLevel().getBlockState(bpos).getValue(BlockScreen.emitting) ? 0 : tes.getLevel().getSignal(bpos, Direction.values()[side.reverse().ordinal()]); cb.success("{\"level\":" + level + "}"); } } else cb.failure(400, "Wrong arguments"); }); register("GetRedstoneArray", (cb, tes, side, args) -> { if(tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_INPUT)) { final Direction facing = Direction.values()[side.reverse().ordinal()]; final StringJoiner resp = new StringJoiner(",", "{\"levels\":[", "]}"); tes.forEachScreenBlocks(side, bp -> { if(tes.getLevel().getBlockState(bp).getValue(BlockScreen.emitting)) resp.add("0"); else resp.add("" + tes.getLevel().getSignal(bp, facing)); }); cb.success(resp.toString()); } else cb.failure(403, "Missing upgrade"); }); register("ClearRedstone", (cb, tes, side, args) -> { if(tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_OUTPUT)) { if(tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())) makeServerQuery(tes, side, cb, JSServerRequest.CLEAR_REDSTONE); else cb.success("{\"status\":\"notOwner\"}"); } else cb.failure(403, "Missing upgrade"); }); register("SetRedstoneAt", (cb, tes, side, args) -> { if(args.length != 3 || !Arrays.stream(args).allMatch((obj) -> obj instanceof Double)) { cb.failure(400, "Wrong arguments"); return; } if(!tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_OUTPUT)) { cb.failure(403, "Missing upgrade"); return; } if(!tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())) { cb.success("{\"status\":\"notOwner\"}"); return; } int x = ((Double) args[0]).intValue(); int y = ((Double) args[1]).intValue(); boolean state = ((Double) args[2]) > 0.0; Vector2i size = tes.getScreen(side).size; if(x < 0 || x >= size.x || y < 0 || y >= size.y) { cb.failure(403, "Out of range"); return; } makeServerQuery(tes, side, cb, JSServerRequest.SET_REDSTONE_AT, x, y, state); }); register("IsEmitting", (cb, tes, side, args) -> { if(!tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_OUTPUT)) { cb.failure(403, "Missing upgrade"); return; } if(args.length == 2 && args[0] instanceof Double && args[1] instanceof Double) { TileEntityScreen.Screen scr = tes.getScreen(side); int x = ((Double) args[0]).intValue(); int y = ((Double) args[1]).intValue(); if(x < 0 || x >= scr.size.x || y < 0 || y >= scr.size.y) cb.failure(403, "Out of range"); else { BlockPos bpos = (new Vector3i(tes.getBlockPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); boolean e = tes.getLevel().getBlockState(bpos).getValue(BlockScreen.emitting); cb.success("{\"emitting\":" + (e ? "true" : "false") + "}"); } } else cb.failure(400, "Wrong arguments"); }); register("GetEmissionArray", (cb, tes, side, args) -> { if(tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_OUTPUT)) { final StringJoiner resp = new StringJoiner(",", "{\"emission\":[", "]}"); tes.forEachScreenBlocks(side, bp -> resp.add(tes.getLevel().getBlockState(bp).getValue(BlockScreen.emitting) ? "1" : "0")); cb.success(resp.toString()); } else cb.failure(403, "Missing upgrade"); }); register("GetLocation", (cb, tes, side, args) -> { if(!tes.hasUpgrade(side, DefaultUpgrade.GPS)) { cb.failure(403, "Missing upgrade"); return; } BlockPos bp = tes.getBlockPos(); cb.success("{\"x\":" + bp.getX() + ",\"y\":" + bp.getY() + ",\"z\":" + bp.getZ() + ",\"side\":\"" + side + "\"}"); }); register("GetUpgrades", (cb, tes, side, args) -> { final StringBuilder sb = new StringBuilder("{\"upgrades\":["); final ArrayList upgrades = tes.getScreen(side).upgrades; for(int i = 0; i < upgrades.size(); i++) { if(i > 0) sb.append(','); sb.append('\"'); sb.append(Util.addSlashes(((IUpgrade) upgrades.get(i).getItem()).getJSName(upgrades.get(i)))); sb.append('\"'); } cb.success(sb.append("]}").toString()); }); register("IsOwner", (cb, tes, side, args) -> { boolean res = (tes.getScreen(side).owner != null && tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())); cb.success("{\"isOwner\":" + (res ? "true}" : "false}")); }); register("GetRotation", (cb, tes, side, args) -> cb.success("{\"rotation\":" + tes.getScreen(side).rotation.ordinal() + "}")); register("GetSide", (cb, tes, side, args) -> cb.success("{\"side\":" + tes.getScreen(side).side.ordinal() + "}")); } }