diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2669d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +This mod and its source code is now in public domain. Feel free to do whatever you want with it; make forks, distribute it... whatever I would appreciate it, of course, if you credited me 😊 Thank you! diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ebf464 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +## Simple Voice Chat - A working voice chat in Minecraft! + +### Features + Push to talk + Voice activation + Proximity voice chat + Password protected group chats + Opus codec + RNNoise recurrent neural network noise suppression + OpenAL audio + Cross compatibility between Fabric, NeoForge, Forge, Quilt, Bukkit, Spigot and Paper + Support for Velocity, BungeeCord and Waterfall + Compatibility with ModMenu (Use ClothConfig for a better configuration UI) + Configurable push to talk key + Microphone test playback + Configurable voice distance + Whispering + Individual player volume adjustment + Microphone amplification + 3D sound + AES encryption + Audio recording with separate audio tracks + A powerful API diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7b9db99 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + jcenter() + maven { url = "http://files.minecraftforge.net/maven" } + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' + } +} +apply plugin: 'net.minecraftforge.gradle.forge' +//Only edit below this line, the above code adds and enables the necessary things for Forge to be setup. + + +version = "1.12.2-1.1" +group = "net.montoyo.wd" // http://maven.apache.org/guides/mini/guide-naming-conventions.html +archivesBaseName = "webdisplays" + +sourceCompatibility = targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. +compileJava { + sourceCompatibility = targetCompatibility = '1.8' +} + +minecraft { + version = "1.12.2-14.23.5.2768" + runDir = "run" + + // the mappings can be changed at any time, and must be in the following format. + // snapshot_YYYYMMDD snapshot are built nightly. + // stable_# stables are built at the discretion of the MCP team. + // Use non-default mappings at your own risk. they may not always work. + // simply re-run your setup task after changing the mappings to update your workspace. + mappings = "snapshot_20171003" + // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable. +} + +dependencies { +} + +jar { + exclude 'org/**' + exclude 'net/montoyo/mcef/**' + exclude 'assets/mcef/**' +} + +processResources { + // this will ensure that this task is redone when the versions change. + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + + // replace stuff in mcmod.info, nothing else + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + + // replace version and mcversion + expand 'version':project.version, 'mcversion':project.minecraft.version + } + + // copy everything else except the mcmod.info + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e9b9fd5 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +# This is required to provide enough memory for the Minecraft decompilation process. +org.gradle.jvmargs=-Xmx3G diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/ex/terrible/horse/SharedProxy.java b/src/main/java/ex/terrible/horse/SharedProxy.java new file mode 100644 index 0000000..1200cc5 --- /dev/null +++ b/src/main/java/ex/terrible/horse/SharedProxy.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd; + +import com.mojang.authlib.GameProfile; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.common.DimensionManager; +import net.minecraftforge.fml.server.FMLServerHandler; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.core.HasAdvancement; +import net.montoyo.wd.core.JSServerRequest; +import net.montoyo.wd.data.GuiData; +import net.montoyo.wd.entity.TileEntityScreen; +import net.montoyo.wd.utilities.*; + +import javax.annotation.Nonnull; + +public class SharedProxy { + + public static final int CURRENT_DIMENSION = Integer.MAX_VALUE; + + public void preInit() { + } + + public void init() { + } + + public void postInit() { + } + + public World getWorld(int dim) { + if(dim == CURRENT_DIMENSION) + throw new RuntimeException("Current dimension not available server side..."); + + return DimensionManager.getWorld(dim); + } + + public void enqueue(Runnable r) { + FMLServerHandler.instance().getServer().addScheduledTask(r); + } + + public void displayGui(GuiData data) { + Log.error("Called SharedProxy.displayGui() on server side..."); + } + + public void trackScreen(TileEntityScreen tes, boolean track) { + } + + public void onAutocompleteResult(NameUUIDPair pairs[]) { + } + + public GameProfile[] getOnlineGameProfiles() { + return FMLServerHandler.instance().getServer().getOnlinePlayerProfiles(); + } + + public void screenUpdateResolutionInGui(Vector3i pos, BlockSide side, Vector2i res) { + } + + public void screenUpdateRotationInGui(Vector3i pos, BlockSide side, Rotation rot) { + } + + public void screenUpdateAutoVolumeInGui(Vector3i pos, BlockSide side, boolean av) { + } + + public void displaySetPadURLGui(String padURL) { + Log.error("Called SharedProxy.displaySetPadURLGui() on server side..."); + } + + public void openMinePadGui(int padId) { + Log.error("Called SharedProxy.openMinePadGui() on server side..."); + } + + public void handleJSResponseSuccess(int reqId, JSServerRequest type, byte[] data) { + Log.error("Called SharedProxy.handleJSResponseSuccess() on server side..."); + } + + public void handleJSResponseError(int reqId, JSServerRequest type, int errCode, String err) { + Log.error("Called SharedProxy.handleJSResponseError() on server side..."); + } + + @Nonnull + public HasAdvancement hasClientPlayerAdvancement(@Nonnull ResourceLocation rl) { + return HasAdvancement.DONT_KNOW; + } + + public MinecraftServer getServer() { + return FMLServerHandler.instance().getServer(); + } + + public void setMiniservClientPort(int port) { + } + + public void startMiniservClient() { + } + + public boolean isMiniservDisabled() { + return false; + } + + public void closeGui(BlockPos bp, BlockSide bs) { + } + + public void renderRecipes() { + } + + public boolean isShiftDown() { + return false; + } + +} diff --git a/src/main/java/ex/terrible/horse/Terrible.java b/src/main/java/ex/terrible/horse/Terrible.java new file mode 100644 index 0000000..4aae24a --- /dev/null +++ b/src/main/java/ex/terrible/horse/Terrible.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd; + +import com.google.gson.Gson; +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.block.Block; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.init.SoundEvents; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.SoundCategory; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.client.event.ClientChatEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.event.ServerChatEvent; +import net.minecraftforge.event.entity.item.ItemTossEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.SidedProxy; +import net.minecraftforge.fml.common.event.*; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.PlayerEvent; +import net.minecraftforge.fml.common.network.NetworkRegistry; +import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper; +import net.minecraftforge.fml.common.registry.GameRegistry; +import net.montoyo.wd.block.BlockKeyboardRight; +import net.montoyo.wd.block.BlockPeripheral; +import net.montoyo.wd.block.BlockScreen; +import net.montoyo.wd.core.*; +import net.montoyo.wd.entity.TileEntityScreen; +import net.montoyo.wd.item.*; +import net.montoyo.wd.miniserv.server.Server; +import net.montoyo.wd.net.client.CMessageServerInfo; +import net.montoyo.wd.net.Messages; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Util; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.UUID; + +@Mod(modid = "webdisplays", version = WebDisplays.MOD_VERSION, dependencies = "required-after:mcef@[1.0,2.0);after:opencomputers;after:computercraft;") +public class WebDisplays { + + public static final String MOD_VERSION = "1.1"; + + @Mod.Instance(owner = "webdisplays") + public static WebDisplays INSTANCE; + + @SidedProxy(serverSide = "net.montoyo.wd.SharedProxy", clientSide = "net.montoyo.wd.client.ClientProxy") + public static SharedProxy PROXY; + + public static SimpleNetworkWrapper NET_HANDLER; + public static WDCreativeTab CREATIVE_TAB; + public static final ResourceLocation ADV_PAD_BREAK = new ResourceLocation("webdisplays", "webdisplays/pad_break"); + public static final String BLACKLIST_URL = "mod://webdisplays/blacklisted.html"; + public static final Gson GSON = new Gson(); + public static final ResourceLocation CAPABILITY = new ResourceLocation("webdisplays", "customdatacap"); + + //Blocks + public BlockScreen blockScreen; + public BlockPeripheral blockPeripheral; + public BlockKeyboardRight blockKbRight; + + //Items + public ItemScreenConfigurator itemScreenCfg; + public ItemOwnershipThief itemOwnerThief; + public ItemLinker itemLinker; + public ItemMinePad2 itemMinePad; + public ItemUpgrade itemUpgrade; + public ItemLaserPointer itemLaserPointer; + public ItemCraftComponent itemCraftComp; + public ItemMulti itemAdvIcon; + + //Sounds + public SoundEvent soundTyping; + public SoundEvent soundUpgradeAdd; + public SoundEvent soundUpgradeDel; + public SoundEvent soundScreenCfg; + public SoundEvent soundServer; + public SoundEvent soundIronic; + + //Criterions + public Criterion criterionPadBreak; + public Criterion criterionUpgradeScreen; + public Criterion criterionLinkPeripheral; + public Criterion criterionKeyboardCat; + + //Config + public static final double PAD_RATIO = 59.0 / 30.0; + public String homePage; + public double padResX; + public double padResY; + private int lastPadId = 0; + public boolean doHardRecipe; + private boolean hasOC; + private boolean hasCC; + private String[] blacklist; + public boolean disableOwnershipThief; + public double unloadDistance2; + public double loadDistance2; + public int maxResX; + public int maxResY; + public int maxScreenX; + public int maxScreenY; + public int miniservPort; + public long miniservQuota; + public boolean enableSoundDistance; + public float ytVolume; + public float avDist100; + public float avDist0; + + @Mod.EventHandler + public void onPreInit(FMLPreInitializationEvent ev) { + //Load config + Configuration cfg = new Configuration(ev.getSuggestedConfigurationFile()); + cfg.load(); + + //CAT: Main + Property blacklist = cfg.get("main", "blacklist", new String[0]); + Property padHeight = cfg.get("main", "padHeight", 480); + Property hardRecipe = cfg.get("main", "hardRecipes", true); + Property homePage = cfg.get("main", "homepage", "mod://webdisplays/main.html"); + Property disableOT = cfg.get("main", "disableOwnershipThief", false); + Property maxResX = cfg.get("main", "maxResolutionX", 1920); + Property maxResY = cfg.get("main", "maxResolutionY", 1080); + Property miniservPort = cfg.get("main", "miniservPort", 25566); + Property miniservQuota = cfg.get("main", "miniservQuota", 1024); //It's stored as a string anyway + Property maxScreenX = cfg.get("main", "maxScreenSizeX", 16); + Property maxScreenY = cfg.get("main", "maxScreenSizeY", 16); + + //CAT: Client options + Property loadDistance = cfg.get("client", "loadDistance", 30.0); + Property unloadDistance = cfg.get("client", "unloadDistance", 32.0); + + //CAT: Auto volume config (client-side) + Property enableAutoVol = cfg.get("clientAutoVolume", "enableAutoVolume", true); + Property ytVolume = cfg.get("clientAutoVolume", "ytVolume", 100.0); + Property dist100 = cfg.get("clientAutoVolume", "dist100", 10.0); + Property dist0 = cfg.get("clientAutoVolume", "dist0", 30.0); + + + //Comments & shit + blacklist.setComment("An array of domain names you don't want to load."); + padHeight.setComment("The minePad Y resolution in pixels. padWidth = padHeight * " + PAD_RATIO); + hardRecipe.setComment("If true, breaking the minePad is required to craft upgrades."); + homePage.setComment("The URL that will be loaded each time you create a screen"); + disableOT.setComment("If true, the ownership thief item will be disabled"); + loadDistance.setComment("All screens outside this range will be unloaded"); + unloadDistance.setComment("All unloaded screens inside this range will be loaded"); + maxResX.setComment("Maximum horizontal screen resolution, in pixels"); + maxResY.setComment("Maximum vertical screen resolution, in pixels"); + miniservPort.setComment("The port used by miniserv. 0 to disable."); + miniservPort.setMaxValue(Short.MAX_VALUE); + miniservQuota.setComment("The amount of data that can be uploaded to miniserv, in KiB (so 1024 = 1 MiO)"); + maxScreenX.setComment("Maximum screen width, in blocks. Resolution will be clamped by maxResolutionX."); + maxScreenY.setComment("Maximum screen height, in blocks. Resolution will be clamped by maxResolutionY."); + enableAutoVol.setComment("If true, the volume of YouTube videos will change depending on how far you are"); + ytVolume.setComment("Volume for YouTube videos. This will have no effect if enableSoundDistance is set to false"); + ytVolume.setMinValue(0.0); + ytVolume.setMaxValue(100.0); + dist100.setComment("Distance after which the sound starts dropping (in blocks)"); + dist100.setMinValue(0.0); + dist0.setComment("Distance after which you can't hear anything (in blocks)"); + dist0.setMinValue(0.0); + + if(unloadDistance.getDouble() < loadDistance.getDouble() + 2.0) + unloadDistance.set(loadDistance.getDouble() + 2.0); + + if(dist0.getDouble() < dist100.getDouble() + 0.1) + dist0.set(dist100.getDouble() + 0.1); + + cfg.save(); + + this.blacklist = blacklist.getStringList(); + doHardRecipe = hardRecipe.getBoolean(); + this.homePage = homePage.getString(); + disableOwnershipThief = disableOT.getBoolean(); + unloadDistance2 = unloadDistance.getDouble() * unloadDistance.getDouble(); + loadDistance2 = loadDistance.getDouble() * loadDistance.getDouble(); + this.maxResX = maxResX.getInt(); + this.maxResY = maxResY.getInt(); + this.miniservPort = miniservPort.getInt(); + this.miniservQuota = miniservQuota.getLong() * 1024L; + this.maxScreenX = maxScreenX.getInt(); + this.maxScreenY = maxScreenY.getInt(); + enableSoundDistance = enableAutoVol.getBoolean(); + this.ytVolume = (float) ytVolume.getDouble(); + avDist100 = (float) dist100.getDouble(); + avDist0 = (float) dist0.getDouble(); + + CREATIVE_TAB = new WDCreativeTab(); + + //Criterions + criterionPadBreak = new Criterion("pad_break"); + criterionUpgradeScreen = new Criterion("upgrade_screen"); + criterionLinkPeripheral = new Criterion("link_peripheral"); + criterionKeyboardCat = new Criterion("keyboard_cat"); + registerTrigger(criterionPadBreak, criterionUpgradeScreen, criterionLinkPeripheral, criterionKeyboardCat); + + //Read configuration + padResY = (double) padHeight.getInt(); + padResX = padResY * PAD_RATIO; + + //Init blocks + blockScreen = new BlockScreen(); + blockScreen.makeItemBlock(); + + blockPeripheral = new BlockPeripheral(); + blockPeripheral.makeItemBlock(); + + blockKbRight = new BlockKeyboardRight(); + + //Init items + itemScreenCfg = new ItemScreenConfigurator(); + itemOwnerThief = new ItemOwnershipThief(); + itemLinker = new ItemLinker(); + itemMinePad = new ItemMinePad2(); + itemUpgrade = new ItemUpgrade(); + itemLaserPointer = new ItemLaserPointer(); + itemCraftComp = new ItemCraftComponent(); + + itemAdvIcon = new ItemMulti(AdvancementIcon.class); + itemAdvIcon.setUnlocalizedName("webdisplays.advicon"); + itemAdvIcon.setRegistryName("advicon"); + + PROXY.preInit(); + MinecraftForge.EVENT_BUS.register(this); + } + + @Mod.EventHandler + public void onInit(FMLInitializationEvent ev) { + //Register tile entities + GameRegistry.registerTileEntity(TileEntityScreen.class, new ResourceLocation("webdisplays", "screen")); + for(DefaultPeripheral dp: DefaultPeripheral.values()) { + if(dp.getTEClass() != null) + GameRegistry.registerTileEntity(dp.getTEClass(), new ResourceLocation("webdisplays", dp.getName())); + } + + //Other things + CapabilityManager.INSTANCE.register(IWDDCapability.class, new WDDCapability.Storage(), new WDDCapability.Factory()); + PROXY.init(); + NET_HANDLER = NetworkRegistry.INSTANCE.newSimpleChannel("webdisplays"); + Messages.registerAll(NET_HANDLER); + } + + @Mod.EventHandler + public void onPostInit(FMLPostInitializationEvent ev) { + PROXY.postInit(); + hasOC = Loader.isModLoaded("opencomputers"); + hasCC = Loader.isModLoaded("computercraft"); + + if(hasCC) { + try { + //We have to do this because the "register" method might be stripped out if CC isn't loaded + CCPeripheralProvider.class.getMethod("register").invoke(null); + } catch(Throwable t) { + Log.error("ComputerCraft was found, but WebDisplays wasn't able to register its CC Interface Peripheral"); + t.printStackTrace(); + } + } + } + + @SubscribeEvent + public void onRegisterBlocks(RegistryEvent.Register ev) { + ev.getRegistry().registerAll(blockScreen, blockPeripheral, blockKbRight); + } + + @SubscribeEvent + public void onRegisterItems(RegistryEvent.Register ev) { + ev.getRegistry().registerAll(blockScreen.getItem(), blockPeripheral.getItem()); + ev.getRegistry().registerAll(itemScreenCfg, itemOwnerThief, itemLinker, itemMinePad, itemUpgrade, itemLaserPointer, itemCraftComp, itemAdvIcon); + } + + @SubscribeEvent + public void onRegisterSounds(RegistryEvent.Register ev) { + soundTyping = registerSound(ev, "keyboardType"); + soundUpgradeAdd = registerSound(ev, "upgradeAdd"); + soundUpgradeDel = registerSound(ev, "upgradeDel"); + soundScreenCfg = registerSound(ev, "screencfgOpen"); + soundServer = registerSound(ev, "server"); + soundIronic = registerSound(ev, "ironic"); + } + + @SubscribeEvent + public void onWorldLoad(WorldEvent.Load ev) { + if(ev.getWorld().isRemote || ev.getWorld().provider.getDimension() != 0) + return; + + File worldDir = ev.getWorld().getSaveHandler().getWorldDirectory(); + File f = new File(worldDir, "wd_next.txt"); + + if(f.exists()) { + try { + BufferedReader br = new BufferedReader(new FileReader(f)); + String idx = br.readLine(); + Util.silentClose(br); + + if(idx == null) + throw new RuntimeException("Seems like the file is empty (1)"); + + idx = idx.trim(); + if(idx.isEmpty()) + throw new RuntimeException("Seems like the file is empty (2)"); + + lastPadId = Integer.parseInt(idx); //This will throw NumberFormatException if it goes wrong + } catch(Throwable t) { + Log.warningEx("Could not read last minePad ID from %s. I'm afraid this might break all minePads.", t, f.getAbsolutePath()); + } + } + + if(miniservPort != 0) { + Server sv = Server.getInstance(); + sv.setPort(miniservPort); + sv.setDirectory(new File(worldDir, "wd_filehost")); + sv.start(); + } + } + + @SubscribeEvent + public void onWorldSave(WorldEvent.Save ev) { + if(ev.getWorld().isRemote || ev.getWorld().provider.getDimension() != 0) + return; + + File f = new File(ev.getWorld().getSaveHandler().getWorldDirectory(), "wd_next.txt"); + + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(f)); + bw.write("" + lastPadId + "\n"); + Util.silentClose(bw); + } catch(Throwable t) { + Log.warningEx("Could not save last minePad ID (%d) to %s. I'm afraid this might break all minePads.", t, lastPadId, f.getAbsolutePath()); + } + } + + @SubscribeEvent + public void onToss(ItemTossEvent ev) { + if(!ev.getEntityItem().world.isRemote) { + ItemStack is = ev.getEntityItem().getItem(); + + if(is.getItem() == itemMinePad) { + NBTTagCompound tag = is.getTagCompound(); + + if(tag == null) { + tag = new NBTTagCompound(); + is.setTagCompound(tag); + } + + UUID thrower = ev.getPlayer().getGameProfile().getId(); + tag.setLong("ThrowerMSB", thrower.getMostSignificantBits()); + tag.setLong("ThrowerLSB", thrower.getLeastSignificantBits()); + tag.setDouble("ThrowHeight", ev.getPlayer().posY + ev.getPlayer().getEyeHeight()); + } + } + } + + @SubscribeEvent + public void onPlayerCraft(PlayerEvent.ItemCraftedEvent ev) { + if(doHardRecipe && ev.crafting.getItem() == itemCraftComp && ev.crafting.getMetadata() == CraftComponent.EXTENSION_CARD.ordinal()) { + if((ev.player instanceof EntityPlayerMP && !hasPlayerAdvancement((EntityPlayerMP) ev.player, ADV_PAD_BREAK)) || PROXY.hasClientPlayerAdvancement(ADV_PAD_BREAK) != HasAdvancement.YES) { + ev.crafting.setItemDamage(CraftComponent.BAD_EXTENSION_CARD.ordinal()); + + if(!ev.player.world.isRemote) + ev.player.world.playSound(null, ev.player.posX, ev.player.posY, ev.player.posZ, SoundEvents.ENTITY_ITEM_BREAK, SoundCategory.MASTER, 1.0f, 1.0f); + } + } + } + + @Mod.EventHandler + public void onServerStop(FMLServerStoppingEvent ev) { + Server.getInstance().stopServer(); + } + + @SubscribeEvent + public void onLogIn(PlayerEvent.PlayerLoggedInEvent ev) { + if(!ev.player.world.isRemote && ev.player instanceof EntityPlayerMP) { + WebDisplays.NET_HANDLER.sendTo(new CMessageServerInfo(miniservPort), (EntityPlayerMP) ev.player); + IWDDCapability cap = ev.player.getCapability(WDDCapability.INSTANCE, null); + + if(cap == null) + Log.warning("Player %s (%s) has null IWDDCapability!", ev.player.getName(), ev.player.getGameProfile().getId().toString()); + else if(cap.isFirstRun()) { + Util.toast(ev.player, TextFormatting.LIGHT_PURPLE, "welcome1"); + Util.toast(ev.player, TextFormatting.LIGHT_PURPLE, "welcome2"); + Util.toast(ev.player, TextFormatting.LIGHT_PURPLE, "welcome3"); + + cap.clearFirstRun(); + } + } + } + + @SubscribeEvent + public void onLogOut(PlayerEvent.PlayerLoggedOutEvent ev) { + if(!ev.player.world.isRemote) + Server.getInstance().getClientManager().revokeClientKey(ev.player.getGameProfile().getId()); + } + + @SubscribeEvent + public void attachEntityCaps(AttachCapabilitiesEvent ev) { + if(ev.getObject() instanceof EntityPlayer) + ev.addCapability(CAPABILITY, new WDDCapability.Provider()); + } + + @SubscribeEvent + public void onPlayerClone(net.minecraftforge.event.entity.player.PlayerEvent.Clone ev) { + IWDDCapability src = ev.getOriginal().getCapability(WDDCapability.INSTANCE, null); + IWDDCapability dst = ev.getEntityPlayer().getCapability(WDDCapability.INSTANCE, null); + + if(src == null) { + Log.error("src is null"); + return; + } + + if(dst == null) { + Log.error("dst is null"); + return; + } + + src.cloneTo(dst); + } + + @SubscribeEvent + public void onServerChat(ServerChatEvent ev) { + String msg = ev.getMessage().trim().replaceAll("\\s+", " ").toLowerCase(); + StringBuilder sb = new StringBuilder(msg.length()); + for(int i = 0; i < msg.length(); i++) { + char chr = msg.charAt(i); + + if(chr != '.' && chr != ',' && chr != ';' && chr != '!' && chr != '?' && chr != ':' && chr != '\'' && chr != '\"' && chr != '`') + sb.append(chr); + } + + if(sb.toString().equals("ironic he could save others from death but not himself")) { + EntityPlayer ply = ev.getPlayer(); + ply.world.playSound(null, ply.posX, ply.posY, ply.posZ, soundIronic, SoundCategory.PLAYERS, 1.0f, 1.0f); + } + } + + @SubscribeEvent + public void onClientChat(ClientChatEvent ev) { + if(ev.getMessage().equals("!WD render recipes")) + PROXY.renderRecipes(); + } + + private boolean hasPlayerAdvancement(EntityPlayerMP ply, ResourceLocation rl) { + MinecraftServer server = PROXY.getServer(); + if(server == null) + return false; + + Advancement adv = server.getAdvancementManager().getAdvancement(rl); + return adv != null && ply.getAdvancements().getProgress(adv).isDone(); + } + + public static int getNextAvailablePadID() { + return INSTANCE.lastPadId++; + } + + private static SoundEvent registerSound(RegistryEvent.Register ev, String resName) { + ResourceLocation resLoc = new ResourceLocation("webdisplays", resName); + SoundEvent ret = new SoundEvent(resLoc); + ret.setRegistryName(resLoc); + + ev.getRegistry().register(ret); + return ret; + } + + private static void registerTrigger(Criterion ... criteria) { + for(Criterion c: criteria) + CriteriaTriggers.register(c); + } + + public static boolean isOpenComputersAvailable() { + return INSTANCE.hasOC; + } + + public static boolean isComputerCraftAvailable() { + return INSTANCE.hasCC; + } + + public static boolean isSiteBlacklisted(String url) { + try { + URL url2 = new URL(Util.addProtocol(url)); + return Arrays.stream(INSTANCE.blacklist).anyMatch(str -> str.equalsIgnoreCase(url2.getHost())); + } catch(MalformedURLException ex) { + return false; + } + } + + public static String applyBlacklist(String url) { + return isSiteBlacklisted(url) ? BLACKLIST_URL : url; + } + +} + diff --git a/src/main/java/ex/terrible/horse/block/BlockKeyboardRight.java b/src/main/java/ex/terrible/horse/block/BlockKeyboardRight.java new file mode 100644 index 0000000..3f1a9f3 --- /dev/null +++ b/src/main/java/ex/terrible/horse/block/BlockKeyboardRight.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.block.Block; +import net.minecraft.block.material.EnumPushReaction; +import net.minecraft.block.material.Material; +import net.minecraft.block.properties.IProperty; +import net.minecraft.block.properties.PropertyInteger; +import net.minecraft.block.state.BlockStateContainer; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.EnumHand; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.RayTraceResult; +import net.minecraft.world.Explosion; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.core.DefaultPeripheral; +import net.montoyo.wd.core.IPeripheral; +import net.montoyo.wd.entity.TileEntityKeyboard; +import net.montoyo.wd.item.ItemLinker; +import net.montoyo.wd.utilities.BlockSide; +import net.montoyo.wd.utilities.Vector3i; + +import javax.annotation.Nonnull; +import java.util.Random; + +public class BlockKeyboardRight extends Block implements IPeripheral { + + public static final PropertyInteger facing = PropertyInteger.create("facing", 0, 3); + private static final IProperty[] properties = new IProperty[] { facing }; + public static final AxisAlignedBB KEYBOARD_AABB = new AxisAlignedBB(0.0, 0.0, 0.0, 1.0, 1.0 / 16.0, 1.0); + + public BlockKeyboardRight() { + super(Material.ROCK); + setHardness(1.5f); + setResistance(10.f); + setUnlocalizedName("webdisplays.peripheral.keyboard"); + setRegistryName("keyboard"); + fullBlock = false; + } + + @Override + @Nonnull + protected BlockStateContainer createBlockState() { + return new BlockStateContainer(this, properties); + } + + @Override + public int quantityDropped(Random random) { + return 0; + } + + @Override + public boolean isFullCube(IBlockState state) { + return false; + } + + @Override + public boolean isFullBlock(IBlockState state) { + return false; + } + + @Override + public boolean isNormalCube(IBlockState state, IBlockAccess world, BlockPos pos) { + return false; + } + + @Override + public boolean isOpaqueCube(IBlockState state) { + return false; + } + + @Override + public boolean doesSideBlockRendering(IBlockState state, IBlockAccess world, BlockPos pos, EnumFacing face) { + return false; + } + + @Override + @Nonnull + public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos) { + return KEYBOARD_AABB; + } + + @Override + @Nonnull + public IBlockState getStateFromMeta(int meta) { + return getDefaultState().withProperty(facing, meta); + } + + @Override + public int getMetaFromState(IBlockState state) { + return state.getValue(facing); + } + + @Override + @Nonnull + public ItemStack getPickBlock(@Nonnull IBlockState state, RayTraceResult target, @Nonnull World world, @Nonnull BlockPos pos, EntityPlayer player) { + return new ItemStack(WebDisplays.INSTANCE.blockPeripheral, 1, 0); + } + + private TileEntityKeyboard getTileEntity(World world, BlockPos pos) { + for(EnumFacing nf: EnumFacing.HORIZONTALS) { + BlockPos np = pos.add(nf.getDirectionVec()); + IBlockState ns = world.getBlockState(np); + + if(ns.getBlock() instanceof BlockPeripheral && ns.getValue(BlockPeripheral.type) == DefaultPeripheral.KEYBOARD) { + TileEntity te = world.getTileEntity(np); + if(te != null && te instanceof TileEntityKeyboard) + return (TileEntityKeyboard) te; + + break; + } + } + + return null; + } + + @Override + public boolean connect(World world, BlockPos pos, IBlockState state, Vector3i scrPos, BlockSide scrSide) { + TileEntityKeyboard keyboard = getTileEntity(world, pos); + return keyboard != null && keyboard.connect(world, pos, state, scrPos, scrSide); + } + + @Override + @Nonnull + public EnumPushReaction getMobilityFlag(IBlockState state) { + return EnumPushReaction.IGNORE; + } + + public static boolean checkNeighborhood(IBlockAccess world, BlockPos bp, BlockPos ignore) { + for(EnumFacing neighbor: EnumFacing.HORIZONTALS) { + BlockPos np = bp.add(neighbor.getDirectionVec()); + + if(ignore == null || !np.equals(ignore)) { + IBlockState state = world.getBlockState(np); + + if(state.getBlock() instanceof BlockPeripheral) { + if(state.getValue(BlockPeripheral.type) == DefaultPeripheral.KEYBOARD) + return false; + } else if(state.getBlock() instanceof BlockKeyboardRight) + return false; + } + } + + return true; + } + + public void removeLeftPiece(World world, BlockPos pos, boolean dropItem) { + for(EnumFacing nf: EnumFacing.HORIZONTALS) { + BlockPos np = pos.add(nf.getDirectionVec()); + IBlockState ns = world.getBlockState(np); + + if(ns.getBlock() instanceof BlockPeripheral && ns.getValue(BlockPeripheral.type) == DefaultPeripheral.KEYBOARD) { + if(dropItem) + ns.getBlock().dropBlockAsItem(world, np, ns, 0); + + world.setBlockToAir(np); + break; + } + } + } + + @Override + public void neighborChanged(IBlockState state, World world, BlockPos pos, Block neighborType, BlockPos neighbor) { + if(world.isRemote) + return; + + if(neighbor.getX() == pos.getX() && neighbor.getY() == pos.getY() - 1 && neighbor.getZ() == pos.getZ() && world.isAirBlock(neighbor)) { + removeLeftPiece(world, pos, true); + world.setBlockToAir(pos); + } + } + + @Override + public boolean removedByPlayer(@Nonnull IBlockState state, World world, @Nonnull BlockPos pos, @Nonnull EntityPlayer ply, boolean willHarvest) { + if(!world.isRemote) + removeLeftPiece(world, pos, !ply.isCreative()); + + return super.removedByPlayer(state, world, pos, ply, willHarvest); + } + + @Override + public void onBlockDestroyedByExplosion(World world, BlockPos pos, Explosion explosionIn) { + if(!world.isRemote) + removeLeftPiece(world, pos, true); + } + + @Override + public void onEntityCollidedWithBlock(World world, BlockPos pos, IBlockState state, Entity entity) { + double rpos = (entity.posY - ((double) pos.getY())) * 16.0; + if(!world.isRemote && rpos >= 1.0 && rpos <= 2.0 && Math.random() < 0.25) { + TileEntityKeyboard tek = getTileEntity(world, pos); + + if(tek != null) + tek.simulateCat(entity); + } + } + + @Override + public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) { + if(player.isSneaking()) + return false; + + if(player.getHeldItem(hand).getItem() instanceof ItemLinker) + return false; + + TileEntityKeyboard tek = getTileEntity(world, pos); + if(tek != null) + return tek.onRightClick(player, hand, BlockSide.values()[facing.ordinal()]); + + return false; + } + +} diff --git a/src/main/java/ex/terrible/horse/block/BlockPeripheral.java b/src/main/java/ex/terrible/horse/block/BlockPeripheral.java new file mode 100644 index 0000000..49be3ac --- /dev/null +++ b/src/main/java/ex/terrible/horse/block/BlockPeripheral.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.block.Block; +import net.minecraft.block.material.EnumPushReaction; +import net.minecraft.block.material.Material; +import net.minecraft.block.properties.IProperty; +import net.minecraft.block.properties.PropertyEnum; +import net.minecraft.block.properties.PropertyInteger; +import net.minecraft.block.state.BlockStateContainer; +import net.minecraft.block.state.IBlockState; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumBlockRenderType; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.EnumHand; +import net.minecraft.util.NonNullList; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3i; +import net.minecraft.world.Explosion; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; +import net.minecraftforge.fml.common.network.NetworkRegistry; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.core.DefaultPeripheral; +import net.montoyo.wd.entity.*; +import net.montoyo.wd.item.ItemLinker; +import net.montoyo.wd.item.ItemPeripheral; +import net.montoyo.wd.net.client.CMessageCloseGui; +import net.montoyo.wd.utilities.BlockSide; +import net.montoyo.wd.utilities.Log; + +import javax.annotation.Nonnull; + +public class BlockPeripheral extends WDBlockContainer { + + public static final PropertyEnum type = PropertyEnum.create("type", DefaultPeripheral.class); + public static final PropertyInteger facing = PropertyInteger.create("facing", 0, 3); + private static final IProperty[] properties = new IProperty[] { type, facing }; + + public BlockPeripheral() { + super(Material.ROCK); + setHardness(1.5f); + setResistance(10.f); + setCreativeTab(WebDisplays.CREATIVE_TAB); + setName("peripheral"); + } + + @Override + protected ItemBlock createItemBlock() { + return new ItemPeripheral(this); + } + + @Override + @Nonnull + protected BlockStateContainer createBlockState() { + return new BlockStateContainer(this, properties); + } + + @Override + @Nonnull + public IBlockState getStateForPlacement(@Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing nocare, float hitX, + float hitY, float hitZ, int meta, @Nonnull EntityLivingBase placer, EnumHand hand) { + int rot = MathHelper.floor(((double) (placer.rotationYaw * 4.0f / 360.0f)) + 2.5) & 3; + return getDefaultState().withProperty(type, DefaultPeripheral.fromMetadata(meta)).withProperty(facing, rot); + } + + @Override + public void getSubBlocks(CreativeTabs tab, NonNullList list) { + for(DefaultPeripheral dp : DefaultPeripheral.values()) + list.add(new ItemStack(getItem(), 1, dp.toMetadata(0))); + } + + @Override + @Nonnull + public IBlockState getStateFromMeta(int meta) { + DefaultPeripheral dp = DefaultPeripheral.fromMetadata(meta); + IBlockState state = getDefaultState().withProperty(type, dp); + + if(dp.hasFacing()) + state = state.withProperty(facing, (meta >> 2) & 3); + + return state; + } + + @Override + public int getMetaFromState(IBlockState state) { + return state.getValue(type).toMetadata(state.getValue(facing)); + } + + @Override + public TileEntity createNewTileEntity(@Nonnull World world, int meta) { + Class cls = DefaultPeripheral.fromMetadata(meta).getTEClass(); + if(cls == null) + return null; + + try { + return cls.newInstance(); + } catch(Throwable t) { + Log.errorEx("Couldn't instantiate peripheral TileEntity:", t); + } + + return null; + } + + @Override + @Nonnull + public EnumBlockRenderType getRenderType(IBlockState state) { + return EnumBlockRenderType.MODEL; + } + + @Override + public int damageDropped(IBlockState state) { + return state.getValue(type).toMetadata(0); + } + + @Override + public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) { + if(player.isSneaking()) + return false; + + if(player.getHeldItem(hand).getItem() instanceof ItemLinker) + return false; + + TileEntity te = world.getTileEntity(pos); + + if(te instanceof TileEntityPeripheralBase) + return ((TileEntityPeripheralBase) te).onRightClick(player, hand, BlockSide.values()[facing.ordinal()]); + else if(te instanceof TileEntityServer) { + ((TileEntityServer) te).onPlayerRightClick(player); + return true; + } else + return false; + } + + @Override + public boolean isFullCube(IBlockState state) { + return state.getValue(type) != DefaultPeripheral.KEYBOARD; + } + + @Override + public boolean isFullBlock(IBlockState state) { + return state.getValue(type) != DefaultPeripheral.KEYBOARD; + } + + @Override + public boolean isNormalCube(IBlockState state, IBlockAccess world, BlockPos pos) { + return state.getValue(type) != DefaultPeripheral.KEYBOARD; + } + + @Override + public boolean isOpaqueCube(IBlockState state) { + return state.getValue(type) != DefaultPeripheral.KEYBOARD; + } + + @Override + public boolean doesSideBlockRendering(IBlockState state, IBlockAccess world, BlockPos pos, EnumFacing face) { + return state.getValue(type) != DefaultPeripheral.KEYBOARD; + } + + @Override + @Nonnull + public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos) { + return state.getValue(type) == DefaultPeripheral.KEYBOARD ? BlockKeyboardRight.KEYBOARD_AABB : FULL_BLOCK_AABB; + } + + @Override + public void onBlockPlacedBy(World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) { + if(world.isRemote) + return; + + if(state.getValue(type) == DefaultPeripheral.KEYBOARD) { + //Keyboard special treatment + int f = state.getValue(facing); + Vec3i dir = EnumFacing.getHorizontal(f).rotateY().getDirectionVec(); + BlockPos left = pos.add(dir); + BlockPos right = pos.subtract(dir); + + if(!world.isAirBlock(pos.down()) && BlockKeyboardRight.checkNeighborhood(world, pos, null)) { + if(world.isAirBlock(right) && !world.isAirBlock(right.down()) && BlockKeyboardRight.checkNeighborhood(world, right, pos)) { + world.setBlockState(right, WebDisplays.INSTANCE.blockKbRight.getDefaultState().withProperty(BlockKeyboardRight.facing, f)); + return; + } else if(world.isAirBlock(left) && !world.isAirBlock(left.down()) && BlockKeyboardRight.checkNeighborhood(world, left, pos)) { + world.setBlockState(left, state); + world.setBlockState(pos, WebDisplays.INSTANCE.blockKbRight.getDefaultState().withProperty(BlockKeyboardRight.facing, f)); + return; + } + } + + //Not good; remove this shit... + world.setBlockToAir(pos); + if(!(placer instanceof EntityPlayer) || !((EntityPlayer) placer).isCreative()) + dropBlockAsItem(world, pos, state, 0); + } else if(placer instanceof EntityPlayer) { + TileEntity te = world.getTileEntity(pos); + + if(te instanceof TileEntityServer) + ((TileEntityServer) te).setOwner((EntityPlayer) placer); + else if(te instanceof TileEntityInterfaceBase) + ((TileEntityInterfaceBase) te).setOwner((EntityPlayer) placer); + } + } + + @Override + @Nonnull + public EnumPushReaction getMobilityFlag(IBlockState state) { + return EnumPushReaction.IGNORE; + } + + private void removeRightPiece(World world, BlockPos pos) { + for(EnumFacing nf: EnumFacing.HORIZONTALS) { + BlockPos np = pos.add(nf.getDirectionVec()); + + if(world.getBlockState(np).getBlock() instanceof BlockKeyboardRight) { + world.setBlockToAir(np); + break; + } + } + } + + @Override + public void neighborChanged(IBlockState state, World world, BlockPos pos, Block neighborType, BlockPos neighbor) { + TileEntity te = world.getTileEntity(pos); + if(te != null && te instanceof TileEntityPeripheralBase) + ((TileEntityPeripheralBase) te).onNeighborChange(neighborType, neighbor); + + if(world.isRemote || state.getValue(type) != DefaultPeripheral.KEYBOARD) + return; + + if(neighbor.getX() == pos.getX() && neighbor.getY() == pos.getY() - 1 && neighbor.getZ() == pos.getZ() && world.isAirBlock(neighbor)) { + removeRightPiece(world, pos); + world.setBlockToAir(pos); + dropBlockAsItem(world, pos, state, 0); + WebDisplays.NET_HANDLER.sendToAllAround(new CMessageCloseGui(pos), point(world, pos)); + } + } + + @Override + public void onBlockDestroyedByPlayer(World world, BlockPos pos, IBlockState state) { + if(!world.isRemote) { + if(state.getBlock() == this && state.getValue(type) == DefaultPeripheral.KEYBOARD) + removeRightPiece(world, pos); + + WebDisplays.NET_HANDLER.sendToAllAround(new CMessageCloseGui(pos), point(world, pos)); + } + } + + @Override + public void onBlockDestroyedByExplosion(World world, BlockPos pos, Explosion explosion) { + onBlockDestroyedByPlayer(world, pos, world.getBlockState(pos)); + } + + @Override + public void onEntityCollidedWithBlock(World world, BlockPos pos, IBlockState state, Entity entity) { + if(!world.isRemote && world.getBlockState(pos).getValue(type) == DefaultPeripheral.KEYBOARD) { + double rpos = (entity.posY - ((double) pos.getY())) * 16.0; + + if(rpos >= 1.0 && rpos <= 2.0 && Math.random() < 0.25) { + TileEntity te = world.getTileEntity(pos); + + if(te != null && te instanceof TileEntityKeyboard) + ((TileEntityKeyboard) te).simulateCat(entity); + } + } + } + + private static NetworkRegistry.TargetPoint point(World world, BlockPos bp) { + return new NetworkRegistry.TargetPoint(world.provider.getDimension(), (double) bp.getX(), (double) bp.getY(), (double) bp.getZ(), 64.0); + } + +} diff --git a/src/main/java/ex/terrible/horse/block/BlockScreen.java b/src/main/java/ex/terrible/horse/block/BlockScreen.java new file mode 100644 index 0000000..5bdc5ac --- /dev/null +++ b/src/main/java/ex/terrible/horse/block/BlockScreen.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.block.Block; +import net.minecraft.block.material.EnumPushReaction; +import net.minecraft.block.material.Material; +import net.minecraft.block.properties.IProperty; +import net.minecraft.block.properties.PropertyBool; +import net.minecraft.block.properties.PropertyInteger; +import net.minecraft.block.state.BlockStateContainer; +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.util.ITooltipFlag; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumBlockRenderType; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.EnumHand; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.world.Explosion; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; +import net.minecraftforge.common.property.ExtendedBlockState; +import net.minecraftforge.common.property.IExtendedBlockState; +import net.minecraftforge.common.property.IUnlistedProperty; +import net.minecraftforge.common.property.Properties; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.core.DefaultUpgrade; +import net.montoyo.wd.core.ScreenRights; +import net.montoyo.wd.core.IUpgrade; +import net.montoyo.wd.data.SetURLData; +import net.montoyo.wd.entity.TileEntityScreen; +import net.montoyo.wd.item.WDItem; +import net.montoyo.wd.utilities.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public class BlockScreen extends WDBlockContainer { + + public static final PropertyBool hasTE = PropertyBool.create("haste"); + public static final PropertyBool emitting = PropertyBool.create("emitting"); + private static final IProperty[] properties = new IProperty[] { hasTE, emitting }; + public static final IUnlistedProperty[] sideFlags = new IUnlistedProperty[6]; + static { + for(int i = 0; i < sideFlags.length; i++) + sideFlags[i] = Properties.toUnlisted(PropertyInteger.create("neighbor" + i, 0, 15)); + } + + private static final int BAR_BOT = 1; + private static final int BAR_RIGHT = 2; + private static final int BAR_TOP = 4; + private static final int BAR_LEFT = 8; + + public BlockScreen() { + super(Material.ROCK); + setHardness(1.5f); + setResistance(10.f); + setCreativeTab(WebDisplays.CREATIVE_TAB); + setName("screen"); + setDefaultState(blockState.getBaseState().withProperty(hasTE, false).withProperty(emitting, false)); + } + + @Override + @Nonnull + public EnumBlockRenderType getRenderType(IBlockState state) { + return EnumBlockRenderType.MODEL; + } + + @Override + @Nonnull + protected BlockStateContainer createBlockState() { + return new ExtendedBlockState(this, properties, sideFlags); + } + + public static boolean isntScreenBlock(IBlockAccess world, Vector3i pos) { + return world.getBlockState(pos.toBlock()).getBlock() != WebDisplays.INSTANCE.blockScreen; + } + + @Override + @Nonnull + public IBlockState getExtendedState(@Nonnull IBlockState state, IBlockAccess world, BlockPos bpos) { + IExtendedBlockState ret = (IExtendedBlockState) blockState.getBaseState(); + Vector3i pos = new Vector3i(bpos); + + for(BlockSide side : BlockSide.values()) { + int icon = 0; + if(isntScreenBlock(world, side.up.clone().add(pos))) icon |= BAR_TOP; + if(isntScreenBlock(world, side.down.clone().add(pos))) icon |= BAR_BOT; + if(isntScreenBlock(world, side.left.clone().add(pos))) icon |= BAR_LEFT; + if(isntScreenBlock(world, side.right.clone().add(pos))) icon |= BAR_RIGHT; + + ret = ret.withProperty(sideFlags[side.ordinal()], icon); + } + + return ret; + } + + @Override + @Nonnull + public IBlockState getStateFromMeta(int meta) { + return getDefaultState().withProperty(hasTE, (meta & 1) != 0).withProperty(emitting, (meta & 2) != 0); + } + + @Override + public int getMetaFromState(IBlockState state) { + int ret = 0; + if(state.getValue(hasTE)) + ret |= 1; + + if(state.getValue(emitting)) + ret |= 2; + + return ret; + } + + @Override + public boolean onBlockActivated(World world, BlockPos bpos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) { + ItemStack heldItem = player.getHeldItem(hand); + if(heldItem.isEmpty()) + heldItem = null; //Easier to work with + else if(!(heldItem.getItem() instanceof IUpgrade)) + return false; + + if(world.isRemote) + return true; + + boolean sneaking = player.isSneaking(); + Vector3i pos = new Vector3i(bpos); + BlockSide side = BlockSide.values()[facing.ordinal()]; + + Multiblock.findOrigin(world, pos, side, null); + TileEntityScreen te = (TileEntityScreen) world.getTileEntity(pos.toBlock()); + + if(te != null && te.getScreen(side) != null) { + TileEntityScreen.Screen scr = te.getScreen(side); + + if(sneaking) { //Set URL + if((scr.rightsFor(player) & ScreenRights.CHANGE_URL) == 0) + Util.toast(player, "restrictions"); + else + (new SetURLData(pos, scr.side, scr.url)).sendTo((EntityPlayerMP) player); + + return true; + } else if(heldItem != null && !te.hasUpgrade(side, heldItem)) { //Add upgrade + if((scr.rightsFor(player) & ScreenRights.MANAGE_UPGRADES) == 0) { + Util.toast(player, "restrictions"); + return true; + } + + if(te.addUpgrade(side, heldItem, player, false)) { + if(!player.isCreative()) + heldItem.shrink(1); + + Util.toast(player, TextFormatting.AQUA, "upgradeOk"); + if(player instanceof EntityPlayerMP) + WebDisplays.INSTANCE.criterionUpgradeScreen.trigger(((EntityPlayerMP) player).getAdvancements()); + } else + Util.toast(player, "upgradeError"); + + return true; + } else { //Click + if((scr.rightsFor(player) & ScreenRights.CLICK) == 0) { + Util.toast(player, "restrictions"); + return true; + } + + Vector2i tmp = new Vector2i(); + if(hit2pixels(side, bpos, pos, scr, hitX, hitY, hitZ, tmp)) + te.click(side, tmp); + + return true; + } + } else if(sneaking) { + Util.toast(player, "turnOn"); + return true; + } + + Vector2i size = Multiblock.measure(world, pos, side); + if(size.x < 2 || size.y < 2) { + Util.toast(player, "tooSmall"); + return true; + } + + if(size.x > WebDisplays.INSTANCE.maxScreenX || size.y > WebDisplays.INSTANCE.maxScreenY) { + Util.toast(player, "tooBig", WebDisplays.INSTANCE.maxScreenX, WebDisplays.INSTANCE.maxScreenY); + return true; + } + + Vector3i err = Multiblock.check(world, pos, size, side); + if(err != null) { + Util.toast(player, "invalid", err.toString()); + return true; + } + + boolean created = false; + Log.info("Player %s (UUID %s) created a screen at %s of size %dx%d", player.getName(), player.getGameProfile().getId().toString(), pos.toString(), size.x, size.y); + + if(te == null) { + BlockPos bp = pos.toBlock(); + world.setBlockState(bp, world.getBlockState(bp).withProperty(hasTE, true)); + te = (TileEntityScreen) world.getTileEntity(bp); + created = true; + } + + te.addScreen(side, size, null, player, !created); + return true; + } + + @Override + public void neighborChanged(IBlockState state, World world, BlockPos pos, Block block, BlockPos source) { + if(block != this && !world.isRemote && !state.getValue(emitting)) { + for(BlockSide side: BlockSide.values()) { + Vector3i vec = new Vector3i(pos); + Multiblock.findOrigin(world, vec, side, null); + + TileEntityScreen tes = (TileEntityScreen) world.getTileEntity(vec.toBlock()); + if(tes != null && tes.hasUpgrade(side, DefaultUpgrade.REDSTONE_INPUT)) { + EnumFacing facing = EnumFacing.VALUES[side.reverse().ordinal()]; //Opposite face + vec.sub(pos.getX(), pos.getY(), pos.getZ()).neg(); + tes.updateJSRedstone(side, new Vector2i(vec.dot(side.right), vec.dot(side.up)), world.getRedstonePower(pos, facing)); + } + } + } + } + + public static boolean hit2pixels(BlockSide side, BlockPos bpos, Vector3i pos, TileEntityScreen.Screen scr, float hitX, float hitY, float hitZ, Vector2i dst) { + if(side.right.x < 0) + hitX -= 1.f; + + if(side.right.z < 0 || side == BlockSide.TOP || side == BlockSide.BOTTOM) + hitZ -= 1.f; + + Vector3f rel = new Vector3f(bpos.getX(), bpos.getY(), bpos.getZ()); + rel.sub((float) pos.x, (float) pos.y, (float) pos.z); + rel.add(hitX, hitY, hitZ); + + float cx = rel.dot(side.right.toFloat()) - 2.f / 16.f; + float cy = rel.dot(side.up.toFloat()) - 2.f / 16.f; + float sw = ((float) scr.size.x) - 4.f / 16.f; + float sh = ((float) scr.size.y) - 4.f / 16.f; + + cx /= sw; + cy /= sh; + + if(cx >= 0.f && cx <= 1.0 && cy >= 0.f && cy <= 1.f) { + if(side != BlockSide.BOTTOM) + cy = 1.f - cy; + + switch(scr.rotation) { + case ROT_90: + cy = 1.0f - cy; + break; + + case ROT_180: + cx = 1.0f - cx; + cy = 1.0f - cy; + break; + + case ROT_270: + cx = 1.0f - cx; + break; + + default: + break; + } + + cx *= (float) scr.resolution.x; + cy *= (float) scr.resolution.y; + + if(scr.rotation.isVertical) { + dst.x = (int) cy; + dst.y = (int) cx; + } else { + dst.x = (int) cx; + dst.y = (int) cy; + } + + return true; + } + + return false; + } + + @Nullable + @Override + public TileEntity createNewTileEntity(@Nonnull World world, int meta) { + if((meta & 1) == 0) + return null; + + return ((meta & 1) == 0) ? null : new TileEntityScreen(); + } + + /************************************************* DESTRUCTION HANDLING *************************************************/ + + private void onDestroy(World world, BlockPos pos, EntityPlayer ply) { + if(!world.isRemote) { + Vector3i bp = new Vector3i(pos); + Multiblock.BlockOverride override = new Multiblock.BlockOverride(bp, Multiblock.OverrideAction.SIMULATE); + + for(BlockSide bs: BlockSide.values()) + destroySide(world, bp.clone(), bs, override, ply); + } + } + + private void destroySide(World world, Vector3i pos, BlockSide side, Multiblock.BlockOverride override, EntityPlayer source) { + Multiblock.findOrigin(world, pos, side, override); + BlockPos bp = pos.toBlock(); + TileEntity te = world.getTileEntity(bp); + + if(te != null && te instanceof TileEntityScreen) { + ((TileEntityScreen) te).onDestroy(source); + world.setBlockState(bp, world.getBlockState(bp).withProperty(hasTE, false)); //Destroy tile entity. + } + } + + @Override + public boolean removedByPlayer(@Nonnull IBlockState state, World world, @Nonnull BlockPos pos, @Nonnull EntityPlayer ply, boolean willHarvest) { + onDestroy(world, pos, ply); + return super.removedByPlayer(state, world, pos, ply, willHarvest); + } + + @Override + public void onBlockDestroyedByExplosion(World world, BlockPos pos, Explosion explosion) { + onDestroy(world, pos, null); + } + + @Override + public void onBlockPlacedBy(World world, BlockPos pos, IBlockState state, EntityLivingBase whoDidThisShit, ItemStack stack) { + if(world.isRemote) + return; + + Multiblock.BlockOverride override = new Multiblock.BlockOverride(new Vector3i(pos), Multiblock.OverrideAction.IGNORE); + Vector3i[] neighbors = new Vector3i[6]; + + neighbors[0] = new Vector3i(pos.getX() + 1, pos.getY(), pos.getZ()); + neighbors[1] = new Vector3i(pos.getX() - 1, pos.getY(), pos.getZ()); + neighbors[2] = new Vector3i(pos.getX(), pos.getY() + 1, pos.getZ()); + neighbors[3] = new Vector3i(pos.getX(), pos.getY() - 1, pos.getZ()); + neighbors[4] = new Vector3i(pos.getX(), pos.getY(), pos.getZ() + 1); + neighbors[5] = new Vector3i(pos.getX(), pos.getY(), pos.getZ() - 1); + + for(Vector3i neighbor: neighbors) { + if(world.getBlockState(neighbor.toBlock()).getBlock() instanceof BlockScreen) { + for(BlockSide bs: BlockSide.values()) + destroySide(world, neighbor.clone(), bs, override, (whoDidThisShit instanceof EntityPlayer) ? ((EntityPlayer) whoDidThisShit) : null); + } + } + } + + @Override + @Nonnull + public EnumPushReaction getMobilityFlag(IBlockState state) { + return EnumPushReaction.IGNORE; + } + + @Override + public int getWeakPower(IBlockState state, IBlockAccess world, BlockPos pos, EnumFacing side) { + return state.getValue(emitting) ? 15 : 0; + } + + @Override + public boolean canProvidePower(IBlockState state) { + return state.getValue(emitting); + } + + @Override + protected ItemBlock createItemBlock() { + return new ItemBlockScreen(this); + } + + private static class ItemBlockScreen extends ItemBlock implements WDItem { + + public ItemBlockScreen(BlockScreen screen) { + super(screen); + } + + @Nullable + @Override + public String getWikiName(@Nonnull ItemStack is) { + return "Screen"; + } + + @Override + public void addInformation(@Nullable ItemStack is, @Nullable World world, @Nullable List tt, @Nullable ITooltipFlag ttFlags) { + super.addInformation(is, world, tt, ttFlags); + WDItem.addInformation(tt); + } + + } + +} diff --git a/src/main/java/ex/terrible/horse/block/WDBlock.java b/src/main/java/ex/terrible/horse/block/WDBlock.java new file mode 100644 index 0000000..4dab4a7 --- /dev/null +++ b/src/main/java/ex/terrible/horse/block/WDBlock.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.block.Block; +import net.minecraft.block.material.MapColor; +import net.minecraft.block.material.Material; +import net.minecraft.item.ItemBlock; + +public abstract class WDBlock extends Block { + + protected ItemBlock itemBlock; + + public WDBlock(Material mat, MapColor color) { + super(mat, color); + } + + public WDBlock(Material material) { + super(material); + } + + protected void setName(String name) { + setUnlocalizedName("webdisplays." + name); + setRegistryName(name); + } + + public void makeItemBlock() { + if(itemBlock != null) + throw new RuntimeException("WDBlock.makeItemBlock() called twice!"); + + itemBlock = new ItemBlock(this); + itemBlock.setUnlocalizedName(getUnlocalizedName()); + itemBlock.setRegistryName(getRegistryName()); + } + + public ItemBlock getItem() { + return itemBlock; + } + +} diff --git a/src/main/java/ex/terrible/horse/block/WDBlockContainer.java b/src/main/java/ex/terrible/horse/block/WDBlockContainer.java new file mode 100644 index 0000000..4e14b43 --- /dev/null +++ b/src/main/java/ex/terrible/horse/block/WDBlockContainer.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.block.BlockContainer; +import net.minecraft.block.material.MapColor; +import net.minecraft.block.material.Material; +import net.minecraft.item.ItemBlock; + +public abstract class WDBlockContainer extends BlockContainer { + + protected ItemBlock itemBlock; + + public WDBlockContainer(Material material) { + super(material); + } + + protected void setName(String name) { + setUnlocalizedName("webdisplays." + name); + setRegistryName(name); + } + + protected abstract ItemBlock createItemBlock(); + + public void makeItemBlock() { + if(itemBlock != null) + throw new RuntimeException("WDBlockContainer.makeItemBlock() called twice!"); + + itemBlock = createItemBlock(); + itemBlock.setUnlocalizedName(getUnlocalizedName()); + itemBlock.setRegistryName(getRegistryName()); + } + + public ItemBlock getItem() { + return itemBlock; + } + +} diff --git a/src/main/java/ex/terrible/horse/client/ClientProxy.java b/src/main/java/ex/terrible/horse/client/ClientProxy.java new file mode 100644 index 0000000..a9e5645 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/ClientProxy.java @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import com.mojang.authlib.GameProfile; +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementProgress; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.multiplayer.ClientAdvancementManager; +import net.minecraft.client.renderer.block.model.ModelResourceLocation; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.SimpleReloadableResourceManager; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.EnumHand; +import net.minecraft.util.EnumHandSide; +import net.minecraft.util.NonNullList; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.RayTraceResult; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraftforge.client.event.*; +import net.minecraftforge.client.model.ModelLoader; +import net.minecraftforge.client.resource.IResourceType; +import net.minecraftforge.client.resource.ISelectiveResourceReloadListener; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.montoyo.mcef.api.*; +import net.montoyo.wd.SharedProxy; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.block.BlockScreen; +import net.montoyo.wd.client.gui.*; +import net.montoyo.wd.client.gui.loading.GuiLoader; +import net.montoyo.wd.client.renderers.*; +import net.montoyo.wd.core.DefaultUpgrade; +import net.montoyo.wd.core.HasAdvancement; +import net.montoyo.wd.core.JSServerRequest; +import net.montoyo.wd.data.GuiData; +import net.montoyo.wd.entity.TileEntityScreen; +import net.montoyo.wd.item.ItemMulti; +import net.montoyo.wd.item.WDItem; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.net.server.SMessagePadCtrl; +import net.montoyo.wd.net.server.SMessageScreenCtrl; +import net.montoyo.wd.utilities.*; +import org.lwjgl.input.Keyboard; +import paulscode.sound.SoundSystemConfig; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.*; +import java.util.function.Predicate; + +public class ClientProxy extends SharedProxy implements ISelectiveResourceReloadListener, IDisplayHandler, IJSQueryHandler { + + public class PadData { + + public IBrowser view; + private boolean isInHotbar; + private final int id; + private long lastURLSent; + + private PadData(String url, int id) { + view = mcef.createBrowser(WebDisplays.applyBlacklist(url)); + view.resize((int) WebDisplays.INSTANCE.padResX, (int) WebDisplays.INSTANCE.padResY); + isInHotbar = true; + this.id = id; + } + + } + + private Minecraft mc; + private final ArrayList modelBakers = new ArrayList<>(); + private net.montoyo.mcef.api.API mcef; + private MinePadRenderer minePadRenderer; + private JSQueryDispatcher jsDispatcher; + private LaserPointerRenderer laserPointerRenderer; + private GuiScreen nextScreen; + private boolean isF1Down; + + //Miniserv handling + private int miniservPort; + private boolean msClientStarted; + + //Client-side advancement hack + private final Field advancementToProgressField = findAdvancementToProgressField(); + private ClientAdvancementManager lastAdvMgr; + private Map advancementToProgress; + + //Laser pointer + private TileEntityScreen pointedScreen; + private BlockSide pointedScreenSide; + private long lastPointPacket; + + //Tracking + private final ArrayList screenTracking = new ArrayList<>(); + private int lastTracked = 0; + + //MinePads Management + private final HashMap padMap = new HashMap<>(); + private final ArrayList padList = new ArrayList<>(); + private int minePadTickCounter = 0; + + /**************************************** INHERITED METHODS ****************************************/ + + @Override + public void preInit() { + mc = Minecraft.getMinecraft(); + MinecraftForge.EVENT_BUS.register(this); + registerCustomBlockBaker(new ScreenBaker(), WebDisplays.INSTANCE.blockScreen); + + mcef = MCEFApi.getAPI(); + if(mcef != null) + mcef.registerScheme("wd", WDScheme.class, true, false, false, true, true, false, false); + } + + @Override + public void init() { + ClientRegistry.bindTileEntitySpecialRenderer(TileEntityScreen.class, new ScreenRenderer()); + jsDispatcher = new JSQueryDispatcher(this); + minePadRenderer = new MinePadRenderer(); + laserPointerRenderer = new LaserPointerRenderer(); + } + + @Override + public void postInit() { + ((SimpleReloadableResourceManager) mc.getResourceManager()).registerReloadListener(this); + + if(mcef == null) + throw new RuntimeException("MCEF is missing"); + + mcef.registerDisplayHandler(this); + mcef.registerJSQueryHandler(this); + findAdvancementToProgressField(); + } + + @Override + public World getWorld(int dim) { + World ret = mc.world; + if(dim == CURRENT_DIMENSION) + return ret; + + if(ret.provider.getDimension() != dim) + throw new RuntimeException("Can't get non-current dimension " + dim + " from client."); + + return ret; + } + + @Override + public void enqueue(Runnable r) { + mc.addScheduledTask(r); + } + + @Override + public void displayGui(GuiData data) { + GuiScreen gui = data.createGui(mc.currentScreen, mc.world); + if(gui != null) + mc.displayGuiScreen(gui); + } + + @Override + public void trackScreen(TileEntityScreen tes, boolean track) { + int idx = -1; + for(int i = 0; i < screenTracking.size(); i++) { + if(screenTracking.get(i) == tes) { + idx = i; + break; + } + } + + if(track) { + if(idx < 0) + screenTracking.add(tes); + } else if(idx >= 0) + screenTracking.remove(idx); + } + + @Override + public void onAutocompleteResult(NameUUIDPair[] pairs) { + if(mc.currentScreen != null && mc.currentScreen instanceof WDScreen) { + if(pairs.length == 0) + ((WDScreen) mc.currentScreen).onAutocompleteFailure(); + else + ((WDScreen) mc.currentScreen).onAutocompleteResult(pairs); + } + } + + @Override + public GameProfile[] getOnlineGameProfiles() { + return new GameProfile[] { mc.player.getGameProfile() }; + } + + @Override + public void screenUpdateResolutionInGui(Vector3i pos, BlockSide side, Vector2i res) { + if(mc.currentScreen != null && mc.currentScreen instanceof GuiScreenConfig) { + GuiScreenConfig gsc = (GuiScreenConfig) mc.currentScreen; + + if(gsc.isForBlock(pos.toBlock(), side)) + gsc.updateResolution(res); + } + } + + @Override + public void screenUpdateRotationInGui(Vector3i pos, BlockSide side, Rotation rot) { + if(mc.currentScreen != null && mc.currentScreen instanceof GuiScreenConfig) { + GuiScreenConfig gsc = (GuiScreenConfig) mc.currentScreen; + + if(gsc.isForBlock(pos.toBlock(), side)) + gsc.updateRotation(rot); + } + } + + @Override + public void screenUpdateAutoVolumeInGui(Vector3i pos, BlockSide side, boolean av) { + if(mc.currentScreen != null && mc.currentScreen instanceof GuiScreenConfig) { + GuiScreenConfig gsc = (GuiScreenConfig) mc.currentScreen; + + if(gsc.isForBlock(pos.toBlock(), side)) + gsc.updateAutoVolume(av); + } + } + + @Override + public void displaySetPadURLGui(String padURL) { + mc.displayGuiScreen(new GuiSetURL2(padURL)); + } + + @Override + public void openMinePadGui(int padId) { + PadData pd = padMap.get(padId); + + if(pd != null && pd.view != null) + mc.displayGuiScreen(new GuiMinePad(pd)); + } + + @Override + @Nonnull + public HasAdvancement hasClientPlayerAdvancement(@Nonnull ResourceLocation rl) { + if(advancementToProgressField != null && mc.player != null && mc.player.connection != null) { + ClientAdvancementManager cam = mc.player.connection.getAdvancementManager(); + Advancement adv = cam.getAdvancementList().getAdvancement(rl); + + if(adv == null) + return HasAdvancement.DONT_KNOW; + + if(lastAdvMgr != cam) { + lastAdvMgr = cam; + + try { + advancementToProgress = (Map) advancementToProgressField.get(cam); + } catch(Throwable t) { + Log.warningEx("Could not get ClientAdvancementManager.advancementToProgress field", t); + advancementToProgress = null; + return HasAdvancement.DONT_KNOW; + } + } + + if(advancementToProgress == null) + return HasAdvancement.DONT_KNOW; + + Object progress = advancementToProgress.get(adv); + if(progress == null) + return HasAdvancement.NO; + + if(!(progress instanceof AdvancementProgress)) { + Log.warning("The ClientAdvancementManager.advancementToProgress map does not contain AdvancementProgress instances"); + advancementToProgress = null; //Invalidate this: it's wrong + return HasAdvancement.DONT_KNOW; + } + + return ((AdvancementProgress) progress).isDone() ? HasAdvancement.YES : HasAdvancement.NO; + } + + return HasAdvancement.DONT_KNOW; + } + + @Override + public MinecraftServer getServer() { + return mc.getIntegratedServer(); + } + + @Override + public void handleJSResponseSuccess(int reqId, JSServerRequest type, byte[] data) { + JSQueryDispatcher.ServerQuery q = jsDispatcher.fulfillQuery(reqId); + + if(q == null) + Log.warning("Received success response for invalid query ID %d of type %s", reqId, type.toString()); + else { + if(type == JSServerRequest.CLEAR_REDSTONE || type == JSServerRequest.SET_REDSTONE_AT) + q.success("{\"status\":\"success\"}"); + else + Log.warning("Received success response for query ID %d, but type is invalid", reqId); + } + } + + @Override + public void handleJSResponseError(int reqId, JSServerRequest type, int errCode, String err) { + JSQueryDispatcher.ServerQuery q = jsDispatcher.fulfillQuery(reqId); + + if(q == null) + Log.warning("Received error response for invalid query ID %d of type %s", reqId, type.toString()); + else + q.error(errCode, err); + } + + @Override + public void setMiniservClientPort(int port) { + miniservPort = port; + } + + @Override + public void startMiniservClient() { + if(miniservPort <= 0) { + Log.warning("Can't start miniserv client: miniserv is disabled"); + return; + } + + if(mc.player == null) { + Log.warning("Can't start miniserv client: player is null"); + return; + } + + SocketAddress saddr = mc.player.connection.getNetworkManager().channel().remoteAddress(); + if(saddr == null || !(saddr instanceof InetSocketAddress)) { + Log.warning("Miniserv client: remote address is not inet, assuming local address"); + saddr = new InetSocketAddress("127.0.0.1", 1234); + } + + InetSocketAddress msAddr = new InetSocketAddress(((InetSocketAddress) saddr).getAddress(), miniservPort); + Client.getInstance().start(msAddr); + msClientStarted = true; + } + + @Override + public boolean isMiniservDisabled() { + return miniservPort <= 0; + } + + @Override + public void closeGui(BlockPos bp, BlockSide bs) { + if(mc.currentScreen instanceof WDScreen) { + WDScreen scr = (WDScreen) mc.currentScreen; + + if(scr.isForBlock(bp, bs)) + mc.displayGuiScreen(null); + } + } + + @Override + public void renderRecipes() { + nextScreen = new RenderRecipe(); + } + + @Override + public boolean isShiftDown() { + return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + } + + /**************************************** RESOURCE MANAGER METHODS ****************************************/ + + @Override + public void onResourceManagerReload(IResourceManager resourceManager, Predicate resourcePredicate) { + Log.info("Resource manager reload: clearing GUI cache..."); + GuiLoader.clearCache(); + } + + /**************************************** DISPLAY HANDLER METHODS ****************************************/ + + @Override + public void onAddressChange(IBrowser browser, String url) { + if(browser != null) { + long t = System.currentTimeMillis(); + + for(PadData pd: padList) { + if(pd.view == browser && t - pd.lastURLSent >= 1000) { + if(WebDisplays.isSiteBlacklisted(url)) + pd.view.loadURL(WebDisplays.BLACKLIST_URL); + else { + pd.lastURLSent = t; //Avoid spamming the server with porn URLs + WebDisplays.NET_HANDLER.sendToServer(new SMessagePadCtrl(pd.id, url)); + } + + break; + } + } + + for(TileEntityScreen tes: screenTracking) + tes.updateClientSideURL(browser, url); + } + } + + @Override + public void onTitleChange(IBrowser browser, String title) { + } + + @Override + public void onTooltip(IBrowser browser, String text) { + } + + @Override + public void onStatusMessage(IBrowser browser, String value) { + } + + /**************************************** JS HANDLER METHODS ****************************************/ + + @Override + public boolean handleQuery(IBrowser browser, long queryId, String query, boolean persistent, IJSQueryCallback cb) { + if(browser != null && persistent && query != null && cb != null) { + query = query.toLowerCase(); + + if(query.startsWith("webdisplays_")) { + query = query.substring(12); + + String args; + int parenthesis = query.indexOf('('); + if(parenthesis < 0) + args = null; + else { + if(query.indexOf(')') != query.length() - 1) { + cb.failure(400, "Malformed request"); + return true; + } + + args = query.substring(parenthesis + 1, query.length() - 1); + query = query.substring(0, parenthesis); + } + + if(jsDispatcher.canHandleQuery(query)) + jsDispatcher.enqueueQuery(browser, query, args, cb); + else + cb.failure(404, "Unknown WebDisplays query"); + + return true; + } + } + + return false; + } + + @Override + public void cancelQuery(IBrowser browser, long queryId) { + } + + /**************************************** EVENT METHODS ****************************************/ + + @SubscribeEvent + public void onStitchTextures(TextureStitchEvent.Pre ev) { + TextureMap texMap = ev.getMap(); + + if(texMap == mc.getTextureMapBlocks()) { + for(ResourceModelPair pair : modelBakers) + pair.getModel().loadTextures(texMap); + } + } + + @SubscribeEvent + public void onBakeModel(ModelBakeEvent ev) { + for(ResourceModelPair pair : modelBakers) + ev.getModelRegistry().putObject(pair.getResourceLocation(), pair.getModel()); + } + + @SubscribeEvent + public void onRegisterModels(ModelRegistryEvent ev) { + final WebDisplays wd = WebDisplays.INSTANCE; + + //I hope I'm doing this right because it doesn't seem like it... + registerItemModel(wd.blockScreen.getItem(), 0, "inventory"); + ModelLoader.setCustomModelResourceLocation(wd.blockPeripheral.getItem(), 0, new ModelResourceLocation("webdisplays:kb_inv", "normal")); + registerItemModel(wd.blockPeripheral.getItem(), 1, "facing=2,type=ccinterface"); + registerItemModel(wd.blockPeripheral.getItem(), 2, "facing=2,type=cointerface"); + registerItemModel(wd.blockPeripheral.getItem(), 3, "facing=0,type=remotectrl"); + registerItemModel(wd.blockPeripheral.getItem(), 7, "facing=0,type=redstonectrl"); + registerItemModel(wd.blockPeripheral.getItem(), 11, "facing=0,type=server"); + registerItemModel(wd.itemScreenCfg, 0, "normal"); + registerItemModel(wd.itemOwnerThief, 0, "normal"); + registerItemModel(wd.itemLinker, 0, "normal"); + registerItemModel(wd.itemMinePad, 0, "normal"); + registerItemModel(wd.itemMinePad, 1, "normal"); + registerItemModel(wd.itemLaserPointer, 0, "normal"); + registerItemMultiModels(wd.itemUpgrade); + registerItemMultiModels(wd.itemCraftComp); + registerItemMultiModels(wd.itemAdvIcon); + } + + @SubscribeEvent + public void onTick(TickEvent.ClientTickEvent ev) { + if(ev.phase == TickEvent.Phase.END) { + //Help + if(Keyboard.isKeyDown(Keyboard.KEY_F1)) { + if(!isF1Down) { + isF1Down = true; + + String wikiName = null; + if(mc.currentScreen instanceof WDScreen) + wikiName = ((WDScreen) mc.currentScreen).getWikiPageName(); + else if(mc.currentScreen instanceof GuiContainer) { + Slot slot = ((GuiContainer) mc.currentScreen).getSlotUnderMouse(); + + if(slot != null && slot.getHasStack() && slot.getStack().getItem() instanceof WDItem) + wikiName = ((WDItem) slot.getStack().getItem()).getWikiName(slot.getStack()); + } + + if(wikiName != null) + mcef.openExampleBrowser("https://montoyo.net/wdwiki/index.php/" + wikiName); + } + } else if(isF1Down) + isF1Down = false; + + //Workaround cuz chat sux + if(nextScreen != null && mc.currentScreen == null) { + mc.displayGuiScreen(nextScreen); + nextScreen = null; + } + + //Unload/load screens depending on client player distance + if(mc.player != null && !screenTracking.isEmpty()) { + int id = lastTracked % screenTracking.size(); + lastTracked++; + + TileEntityScreen tes = screenTracking.get(id); + double dist2 = mc.player.getDistanceSq(tes.getPos()); + + if(tes.isLoaded()) { + if(dist2 > WebDisplays.INSTANCE.unloadDistance2) + tes.unload(); + else if(WebDisplays.INSTANCE.enableSoundDistance) + tes.updateTrackDistance(dist2, SoundSystemConfig.getMasterGain()); + } else if(dist2 <= WebDisplays.INSTANCE.loadDistance2) + tes.load(); + } + + //Load/unload minePads depending on which item is in the player's hand + if(++minePadTickCounter >= 10) { + minePadTickCounter = 0; + EntityPlayer ep = mc.player; + + for(PadData pd: padList) + pd.isInHotbar = false; + + if(ep != null) { + updateInventory(ep.inventory.mainInventory, ep.getHeldItem(EnumHand.MAIN_HAND), 9); + updateInventory(ep.inventory.offHandInventory, ep.getHeldItem(EnumHand.OFF_HAND), 1); //Is this okay? + } + + //TODO: Check for GuiContainer.draggedStack + + for(int i = padList.size() - 1; i >= 0; i--) { + PadData pd = padList.get(i); + + if(!pd.isInHotbar) { + pd.view.close(); + pd.view = null; //This is for GuiMinePad, in case the player dies with the GUI open + padList.remove(i); + padMap.remove(pd.id); + } + } + } + + //Laser pointer raycast + boolean raycastHit = false; + + if(mc.player != null && mc.world != null && mc.player.getHeldItem(EnumHand.MAIN_HAND).getItem() == WebDisplays.INSTANCE.itemLaserPointer + && mc.gameSettings.keyBindUseItem.isKeyDown() + && (mc.objectMouseOver == null || mc.objectMouseOver.typeOfHit != RayTraceResult.Type.BLOCK)) { + laserPointerRenderer.isOn = true; + RayTraceResult result = raycast(64.0); //TODO: Make that distance configurable + + if(result != null) { + BlockPos bpos = result.getBlockPos(); + + if(result.typeOfHit == RayTraceResult.Type.BLOCK && mc.world.getBlockState(bpos).getBlock() == WebDisplays.INSTANCE.blockScreen) { + Vector3i pos = new Vector3i(result.getBlockPos()); + BlockSide side = BlockSide.values()[result.sideHit.ordinal()]; + + Multiblock.findOrigin(mc.world, pos, side, null); + TileEntityScreen te = (TileEntityScreen) mc.world.getTileEntity(pos.toBlock()); + + if(te != null && te.hasUpgrade(side, DefaultUpgrade.LASER_MOUSE)) { //hasUpgrade returns false is there's no screen on side 'side' + //Since rights aren't synchronized, let the server check them for us... + TileEntityScreen.Screen scr = te.getScreen(side); + + if(scr.browser != null) { + float hitX = ((float) result.hitVec.x) - (float) bpos.getX(); + float hitY = ((float) result.hitVec.y) - (float) bpos.getY(); + float hitZ = ((float) result.hitVec.z) - (float) bpos.getZ(); + Vector2i tmp = new Vector2i(); + + if(BlockScreen.hit2pixels(side, bpos, pos, scr, hitX, hitY, hitZ, tmp)) { + laserClick(te, side, scr, tmp); + raycastHit = true; + } + } + } + } + } + } else + laserPointerRenderer.isOn = false; + + if(!raycastHit) + deselectScreen(); + + //Handle JS queries + jsDispatcher.handleQueries(); + + //Miniserv + if(msClientStarted && mc.player == null) { + msClientStarted = false; + Client.getInstance().stop(); + } + } + } + + @SubscribeEvent + public void onRenderPlayerHand(RenderSpecificHandEvent ev) { + Item item = ev.getItemStack().getItem(); + IItemRenderer renderer; + + if(item == WebDisplays.INSTANCE.itemMinePad) + renderer = minePadRenderer; + else if(item == WebDisplays.INSTANCE.itemLaserPointer) + renderer = laserPointerRenderer; + else + return; + + EnumHandSide handSide = mc.player.getPrimaryHand(); + if(ev.getHand() == EnumHand.OFF_HAND) + handSide = handSide.opposite(); + + renderer.render(ev.getItemStack(), (handSide == EnumHandSide.RIGHT) ? 1.0f : -1.0f, ev.getSwingProgress(), ev.getEquipProgress()); + ev.setCanceled(true); + } + + @SubscribeEvent + public void onWorldUnload(WorldEvent.Unload ev) { + Log.info("World unloaded; killing screens..."); + int dim = ev.getWorld().provider.getDimension(); + + for(int i = screenTracking.size() - 1; i >= 0; i--) { + if(screenTracking.get(i).getWorld().provider.getDimension() == dim) //Could be world == ev.getWorld() + screenTracking.remove(i).unload(); + } + } + + /**************************************** OTHER METHODS ****************************************/ + + private void laserClick(TileEntityScreen tes, BlockSide side, TileEntityScreen.Screen scr, Vector2i hit) { + if(pointedScreen == tes && pointedScreenSide == side) { + long t = System.currentTimeMillis(); + + if(t - lastPointPacket >= 100) { + lastPointPacket = t; + WebDisplays.NET_HANDLER.sendToServer(SMessageScreenCtrl.vec2(tes, side, SMessageScreenCtrl.CTRL_LASER_MOVE, hit)); + } + } else { + deselectScreen(); + pointedScreen = tes; + pointedScreenSide = side; + WebDisplays.NET_HANDLER.sendToServer(SMessageScreenCtrl.vec2(tes, side, SMessageScreenCtrl.CTRL_LASER_DOWN, hit)); + } + } + + private void deselectScreen() { + if(pointedScreen != null && pointedScreenSide != null) { + WebDisplays.NET_HANDLER.sendToServer(SMessageScreenCtrl.laserUp(pointedScreen, pointedScreenSide)); + pointedScreen = null; + pointedScreenSide = null; + } + } + + private RayTraceResult raycast(double dist) { + Vec3d start = mc.player.getPositionEyes(1.0f); + Vec3d lookVec = mc.player.getLook(1.0f); + Vec3d end = start.addVector(lookVec.x * dist, lookVec.y * dist, lookVec.z * dist); + + return mc.world.rayTraceBlocks(start, end, true, true, false); + } + + private void updateInventory(NonNullList inv, ItemStack heldStack, int cnt) { + for(int i = 0; i < cnt; i++) { + ItemStack item = inv.get(i); + + if(item.getItem() == WebDisplays.INSTANCE.itemMinePad) { + NBTTagCompound tag = item.getTagCompound(); + + if(tag != null && tag.hasKey("PadID")) + updatePad(tag.getInteger("PadID"), tag, item == heldStack); + } + } + } + + private void registerCustomBlockBaker(IModelBaker baker, Block block0) { + ModelResourceLocation normalLoc = new ModelResourceLocation(block0.getRegistryName(), "normal"); + ResourceModelPair pair = new ResourceModelPair(normalLoc, baker); + modelBakers.add(pair); + ModelLoader.setCustomStateMapper(block0, new StaticStateMapper(normalLoc)); + } + + private void registerItemModel(Item item, int meta, String variant) { + ModelLoader.setCustomModelResourceLocation(item, meta, new ModelResourceLocation(item.getRegistryName(), variant)); + } + + private void registerItemMultiModels(ItemMulti item) { + Enum[] values = item.getEnumValues(); + + for(int i = 0; i < values.length; i++) + ModelLoader.setCustomModelResourceLocation(item, i, new ModelResourceLocation(item.getRegistryName().toString() + '_' + values[i], "normal")); + } + + private void updatePad(int id, NBTTagCompound tag, boolean isSelected) { + PadData pd = padMap.get(id); + + if(pd != null) + pd.isInHotbar = true; + else if(isSelected && tag.hasKey("PadURL")) { + pd = new PadData(tag.getString("PadURL"), id); + padMap.put(id, pd); + padList.add(pd); + } + } + + public MinePadRenderer getMinePadRenderer() { + return minePadRenderer; + } + + public PadData getPadByID(int id) { + return padMap.get(id); + } + + public net.montoyo.mcef.api.API getMCEF() { + return mcef; + } + + public static final class ScreenSidePair { + + public TileEntityScreen tes; + public BlockSide side; + + } + + public boolean findScreenFromBrowser(IBrowser browser, ScreenSidePair pair) { + for(TileEntityScreen tes: screenTracking) { + for(int i = 0; i < tes.screenCount(); i++) { + TileEntityScreen.Screen scr = tes.getScreen(i); + + if(scr.browser == browser) { + pair.tes = tes; + pair.side = scr.side; + return true; + } + } + } + + return false; + } + + private static Field findAdvancementToProgressField() { + Field[] fields = ClientAdvancementManager.class.getDeclaredFields(); + Optional result = Arrays.stream(fields).filter(f -> f.getType() == Map.class).findAny(); + + if(result.isPresent()) { + try { + Field ret = result.get(); + ret.setAccessible(true); + return ret; + } catch(Throwable t) { + t.printStackTrace(); + } + } + + Log.warning("ClientAdvancementManager.advancementToProgress field could not be found"); + return null; + } + +} diff --git a/src/main/java/ex/terrible/horse/client/JSQueryDispatcher.java b/src/main/java/ex/terrible/horse/client/JSQueryDispatcher.java new file mode 100644 index 0000000..ba67596 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/JSQueryDispatcher.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +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.server.SMessageScreenCtrl; +import net.montoyo.wd.utilities.*; + +import java.util.*; + +@SideOnly(Side.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.getMinecraft(); + + 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); + + WebDisplays.NET_HANDLER.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.getPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); + int level = tes.getWorld().getBlockState(bpos).getValue(BlockScreen.emitting) ? 0 : tes.getWorld().getRedstonePower(bpos, EnumFacing.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 EnumFacing facing = EnumFacing.VALUES[side.reverse().ordinal()]; + final StringJoiner resp = new StringJoiner(",", "{\"levels\":[", "]}"); + + tes.forEachScreenBlocks(side, bp -> { + if(tes.getWorld().getBlockState(bp).getValue(BlockScreen.emitting)) + resp.add("0"); + else + resp.add("" + tes.getWorld().getRedstonePower(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.getPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); + boolean e = tes.getWorld().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.getWorld().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.getPos(); + 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() + "}")); + } + +} diff --git a/src/main/java/ex/terrible/horse/client/ResourceModelPair.java b/src/main/java/ex/terrible/horse/client/ResourceModelPair.java new file mode 100644 index 0000000..c1f6664 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/ResourceModelPair.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.minecraft.client.renderer.block.model.ModelResourceLocation; +import net.montoyo.wd.client.renderers.IModelBaker; + +public class ResourceModelPair { + + private final ModelResourceLocation resLoc; + private final IModelBaker model; + + public ResourceModelPair(ModelResourceLocation rl, IModelBaker m) { + resLoc = rl; + model = m; + } + + public ModelResourceLocation getResourceLocation() { + return resLoc; + } + + public IModelBaker getModel() { + return model; + } + +} diff --git a/src/main/java/ex/terrible/horse/client/StaticStateMapper.java b/src/main/java/ex/terrible/horse/client/StaticStateMapper.java new file mode 100644 index 0000000..ad5f825 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/StaticStateMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.renderer.block.model.ModelResourceLocation; +import net.minecraft.client.renderer.block.statemap.StateMapperBase; + +import javax.annotation.Nonnull; + +public class StaticStateMapper extends StateMapperBase { + + private final ModelResourceLocation resLoc; + + public StaticStateMapper(ModelResourceLocation rl) { + resLoc = rl; + } + + @Override + @Nonnull + protected ModelResourceLocation getModelResourceLocation(@Nonnull IBlockState state) { + return resLoc; + } + +} diff --git a/src/main/java/ex/terrible/horse/client/WDScheme.java b/src/main/java/ex/terrible/horse/client/WDScheme.java new file mode 100644 index 0000000..8baa669 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/WDScheme.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.montoyo.mcef.api.IScheme; +import net.montoyo.mcef.api.ISchemeResponseData; +import net.montoyo.mcef.api.ISchemeResponseHeaders; +import net.montoyo.mcef.api.SchemePreResponse; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.miniserv.client.ClientTaskGetFile; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.UUID; + +public class WDScheme implements IScheme { + + private static final String ERROR_PAGE = "

%d %s


Miniserv powered by WebDisplays"; + private ClientTaskGetFile task; + private boolean isErrorPage; + + @Override + public SchemePreResponse processRequest(String url) { + url = url.substring("wd://".length()); + + int pos = url.indexOf('/'); + if(pos < 0) + return SchemePreResponse.NOT_HANDLED; + + String uuidStr = url.substring(0, pos); + String fileStr = url.substring(pos + 1); + + try { + fileStr = URLDecoder.decode(fileStr, "UTF-8"); + } catch(UnsupportedEncodingException ex) { + Log.warningEx("UTF-8 isn't supported... yeah... and I'm a billionaire...", ex); + } + + if(uuidStr.isEmpty() || Util.isFileNameInvalid(fileStr)) + return SchemePreResponse.NOT_HANDLED; + + UUID uuid; + try { + uuid = UUID.fromString(uuidStr); + } catch(IllegalArgumentException ex) { + return SchemePreResponse.NOT_HANDLED; //Invalid UUID + } + + task = new ClientTaskGetFile(uuid, fileStr); + return Client.getInstance().addTask(task) ? SchemePreResponse.HANDLED_CONTINUE : SchemePreResponse.NOT_HANDLED; + } + + @Override + public void getResponseHeaders(ISchemeResponseHeaders resp) { + Log.info("Waiting for response..."); + int status = task.waitForResponse(); + Log.info("Got response %d", status); + + if(status == 0) { + //OK + int extPos = task.getFileName().lastIndexOf('.'); + if(extPos >= 0) { + String mime = ((ClientProxy) WebDisplays.PROXY).getMCEF().mimeTypeFromExtension(task.getFileName().substring(extPos + 1)); + + if(mime != null) + resp.setMimeType(mime); + } + + resp.setStatus(200); + resp.setStatusText("OK"); + resp.setResponseLength(-1); + return; + } + + int errCode; + String errStr; + + if(status == Constants.GETF_STATUS_NOT_FOUND) { + errCode = 404; + errStr = "Not Found"; + } else { + errCode = 500; + errStr = "Internal Server Error"; + } + + resp.setStatus(errCode); + resp.setStatusText(errStr); + + try { + dataToWrite = String.format(ERROR_PAGE, errCode, errStr).getBytes("UTF-8"); + dataOffset = 0; + amountToWrite = dataToWrite.length; + isErrorPage = true; + resp.setResponseLength(amountToWrite); + } catch(UnsupportedEncodingException ex) { + resp.setResponseLength(0); + } + } + + private byte[] dataToWrite; + private int dataOffset; + private int amountToWrite; + + @Override + public boolean readResponse(ISchemeResponseData data) { + if(dataToWrite == null) { + if(isErrorPage) { + data.setAmountRead(0); + return false; + } + + dataToWrite = task.waitForData(); + dataOffset = 3; //packet ID + size + amountToWrite = task.getDataLength(); + + if(amountToWrite <= 0) { + dataToWrite = null; + data.setAmountRead(0); + return false; + } + } + + int toWrite = data.getBytesToRead(); + if(toWrite > amountToWrite) + toWrite = amountToWrite; + + System.arraycopy(dataToWrite, dataOffset, data.getDataArray(), 0, toWrite); + data.setAmountRead(toWrite); + + dataOffset += toWrite; + amountToWrite -= toWrite; + + if(amountToWrite <= 0) { + if(!isErrorPage) + task.nextData(); + + dataToWrite = null; + } + + return true; + } + +} diff --git a/src/main/java/ex/terrible/horse/client/gui/CommandHandler.java b/src/main/java/ex/terrible/horse/client/gui/CommandHandler.java new file mode 100644 index 0000000..ed4f299 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/gui/CommandHandler.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandHandler { + + String value(); + +} diff --git a/src/main/java/ex/terrible/horse/client/gui/controls/BasicControl.java b/src/main/java/ex/terrible/horse/client/gui/controls/BasicControl.java new file mode 100644 index 0000000..7d886e9 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/gui/controls/BasicControl.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui.controls; + +import net.montoyo.wd.client.gui.loading.JsonOWrapper; + +public abstract class BasicControl extends Control { + + protected int x; + protected int y; + protected boolean visible = true; + protected boolean disabled = false; + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public void setPos(int x, int y) { + this.x = x; + this.y = y; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public void enable() { + disabled = false; + } + + public void disable() { + disabled = true; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public void show() { + visible = true; + } + + public void hide() { + visible = false; + } + + @Override + public void load(JsonOWrapper json) { + super.load(json); + x = json.getInt("x", 0); + y = json.getInt("y", 0); + disabled = json.getBool("disabled", false); + visible = json.getBool("visible", true); + } + +} diff --git a/src/main/java/ex/terrible/horse/client/gui/controls/Button.java b/src/main/java/ex/terrible/horse/client/gui/controls/Button.java new file mode 100644 index 0000000..007df39 --- /dev/null +++ b/src/main/java/ex/terrible/horse/client/gui/controls/Button.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui.controls; + +import net.minecraft.client.gui.GuiButton; +import net.montoyo.wd.client.gui.loading.JsonOWrapper; +import org.lwjgl.input.Keyboard; + +public class Button extends Control { + + protected final GuiButton btn; + protected boolean selected = false; + protected boolean shiftDown = false; + protected int originalColor = 0; + protected int shiftColor = 0; + + public static class ClickEvent extends Event