diff options
author | zaaarf <me@zaaarf.foo> | 2024-01-10 03:23:24 +0100 |
---|---|---|
committer | zaaarf <me@zaaarf.foo> | 2024-01-10 03:23:24 +0100 |
commit | 067b2e537deac2d053b0c6b1cfb22e7f191830c7 (patch) | |
tree | 2caaf4c4d0a70d45745c9c177fefc1a6e49da43e /src/main | |
parent | 63cfbaa56ccf67da7b65cf058a68a4b843af5748 (diff) |
feat: basic implementation of macro system/Bindings
Co-authored-by: alemi <me@alemi.dev>
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/java/com/badlogic/gdx/utils/SharedLibraryLoadRuntimeException.java | 32 | ||||
-rw-r--r-- | src/main/java/com/badlogic/gdx/utils/SharedLibraryLoader.java | 355 | ||||
-rw-r--r-- | src/main/java/ftbsc/bscv/Boscovicino.java | 17 | ||||
-rw-r--r-- | src/main/java/ftbsc/bscv/system/Bindings.java | 89 | ||||
-rw-r--r-- | src/main/java/ftbsc/bscv/system/Macros.java | 50 | ||||
-rw-r--r-- | src/main/java/ftbsc/bscv/system/ModManager.java | 10 |
6 files changed, 548 insertions, 5 deletions
diff --git a/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoadRuntimeException.java b/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoadRuntimeException.java new file mode 100644 index 0000000..6af5474 --- /dev/null +++ b/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoadRuntimeException.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package com.badlogic.gdx.utils; + +public class SharedLibraryLoadRuntimeException extends RuntimeException { + private static final long serialVersionUID = 8263101105331379889L; + + public SharedLibraryLoadRuntimeException(String message) { + super(message); + } + + public SharedLibraryLoadRuntimeException(Throwable t) { + super(t); + } + + public SharedLibraryLoadRuntimeException(String message, Throwable t) { + super(message, t); + } +} diff --git a/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoader.java b/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoader.java new file mode 100644 index 0000000..554b862 --- /dev/null +++ b/src/main/java/com/badlogic/gdx/utils/SharedLibraryLoader.java @@ -0,0 +1,355 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +package com.badlogic.gdx.utils; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Random; +import java.util.UUID; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Loads shared libraries from a natives jar file (desktop) or arm folders (Android). For desktop projects, have the natives jar + * in the classpath, for Android projects put the shared libraries in the libs/armeabi and libs/armeabi-v7a folders. + * @author mzechner + * @author Nathan Sweet */ +public class SharedLibraryLoader { + static public boolean isWindows = System.getProperty("os.name").contains("Windows"); + static public boolean isLinux = System.getProperty("os.name").contains("Linux"); + static public boolean isMac = System.getProperty("os.name").contains("Mac"); + static public boolean isIos = false; + static public boolean isAndroid = false; + static public boolean isARM = System.getProperty("os.arch").startsWith("arm") || System.getProperty("os.arch").startsWith("aarch64"); + static public boolean is64Bit = System.getProperty("os.arch").contains("64") || System.getProperty("os.arch").startsWith("armv8"); + + static { + boolean isMOEiOS = System.getProperty("moe.platform.name") != null; + String vm = System.getProperty("java.runtime.name"); + if (vm != null && vm.contains("Android Runtime")) { + isAndroid = true; + isWindows = false; + isLinux = false; + isMac = false; + is64Bit = false; + } + if (isMOEiOS || (!isAndroid && !isWindows && !isLinux && !isMac)) { + isIos = true; + isAndroid = false; + isWindows = false; + isLinux = false; + isMac = false; + is64Bit = false; + } + } + + static private final HashSet<String> loadedLibraries = new HashSet<>(); + static private final Random random = new Random(); + + private String nativesJar; + + public SharedLibraryLoader () { + } + + static String randomUUID () { + return new UUID(random.nextLong(), random.nextLong()).toString(); + } + + /** Fetches the natives from the given natives jar file. Used for testing a shared lib on the fly. + * @param nativesJar */ + public SharedLibraryLoader (String nativesJar) { + this.nativesJar = nativesJar; + } + + /** Returns a CRC of the remaining bytes in the stream. */ + public String crc (InputStream input) { + if (input == null) throw new IllegalArgumentException("input cannot be null."); + CRC32 crc = new CRC32(); + byte[] buffer = new byte[4096]; + try { + while (true) { + int length = input.read(buffer); + if (length == -1) break; + crc.update(buffer, 0, length); + } + } catch (Exception ex) { + } finally { + closeQuietly(input); + } + return Long.toString(crc.getValue(), 16); + } + + /** Maps a platform independent library name to a platform dependent name. */ + public String mapLibraryName (String libraryName) { + if (isWindows) return libraryName + (is64Bit ? "64.dll" : ".dll"); + if (isLinux) return "lib" + libraryName + (isARM ? "arm" : "") + (is64Bit ? "64.so" : ".so"); + if (isMac) return "lib" + libraryName + (isARM ? "arm" : "") + (is64Bit ? "64.dylib" : ".dylib"); + return libraryName; + } + + /** Loads a shared library for the platform the application is running on. + * @param libraryName The platform independent library name. If not contain a prefix (eg lib) or suffix (eg .dll). */ + public void load (String libraryName) { + // in case of iOS, things have been linked statically to the executable, bail out. + if (isIos) return; + + synchronized (SharedLibraryLoader.class) { + if (isLoaded(libraryName)) return; + String platformName = mapLibraryName(libraryName); + try { + if (isAndroid) + System.loadLibrary(platformName); + else + loadFile(platformName); + setLoaded(libraryName); + } catch (Throwable ex) { + throw new SharedLibraryLoadRuntimeException("Couldn't load shared library '" + platformName + "' for target: " + + (isAndroid ? "Android" : (System.getProperty("os.name") + (isARM ? ", ARM" : "") + (is64Bit ? ", 64-bit" : ", 32-bit"))), + ex); + } + } + } + + private InputStream readFile (String path) { + if (nativesJar == null) { + InputStream input = SharedLibraryLoader.class.getResourceAsStream("/" + path); + if (input == null) throw new SharedLibraryLoadRuntimeException("Unable to read file for extraction: " + path); + return input; + } + + // Read from JAR. + try { + ZipFile file = new ZipFile(nativesJar); + ZipEntry entry = file.getEntry(path); + if (entry == null) throw new SharedLibraryLoadRuntimeException("Couldn't find '" + path + "' in JAR: " + nativesJar); + return file.getInputStream(entry); + } catch (IOException ex) { + throw new SharedLibraryLoadRuntimeException("Error reading '" + path + "' in JAR: " + nativesJar, ex); + } + } + + /** Extracts the specified file to the specified directory if it does not already exist or the CRC does not match. If file + * extraction fails and the file exists at java.library.path, that file is returned. + * @param sourcePath The file to extract from the classpath or JAR. + * @param dirName The name of the subdirectory where the file will be extracted. If null, the file's CRC will be used. + * @return The extracted file. */ + public File extractFile (String sourcePath, String dirName) throws IOException { + try { + String sourceCrc = crc(readFile(sourcePath)); + if (dirName == null) dirName = sourceCrc; + + File extractedFile = getExtractedFile(dirName, new File(sourcePath).getName()); + if (extractedFile == null) { + extractedFile = getExtractedFile(randomUUID(), new File(sourcePath).getName()); + if (extractedFile == null) throw new SharedLibraryLoadRuntimeException( + "Unable to find writable path to extract file. Is the user home directory writable?"); + } + return extractFile(sourcePath, sourceCrc, extractedFile); + } catch (RuntimeException ex) { + // Fallback to file at java.library.path location, eg for applets. + File file = new File(System.getProperty("java.library.path"), sourcePath); + if (file.exists()) return file; + throw ex; + } + } + + /** Extracts the specified file into the temp directory if it does not already exist or the CRC does not match. If file + * extraction fails and the file exists at java.library.path, that file is returned. + * @param sourcePath The file to extract from the classpath or JAR. + * @param dir The location where the extracted file will be written. */ + public void extractFileTo (String sourcePath, File dir) throws IOException { + extractFile(sourcePath, crc(readFile(sourcePath)), new File(dir, new File(sourcePath).getName())); + } + + /** Returns a path to a file that can be written. Tries multiple locations and verifies writing succeeds. + * @return null if a writable path could not be found. */ + private File getExtractedFile (String dirName, String fileName) { + // Temp directory with username in path. + File idealFile = new File( + System.getProperty("java.io.tmpdir") + "/libgdx" + System.getProperty("user.name") + "/" + dirName, fileName); + if (canWrite(idealFile)) return idealFile; + + // System provided temp directory. + try { + File file = File.createTempFile(dirName, null); + if (file.delete()) { + file = new File(file, fileName); + if (canWrite(file)) return file; + } + } catch (IOException ignored) { + } + + // User home. + File file = new File(System.getProperty("user.home") + "/.libgdx/" + dirName, fileName); + if (canWrite(file)) return file; + + // Relative directory. + file = new File(".temp/" + dirName, fileName); + if (canWrite(file)) return file; + + // We are running in the OS X sandbox. + if (System.getenv("APP_SANDBOX_CONTAINER_ID") != null) return idealFile; + + return null; + } + + /** Returns true if the parent directories of the file can be created and the file can be written. */ + private boolean canWrite (File file) { + File parent = file.getParentFile(); + File testFile; + if (file.exists()) { + if (!file.canWrite() || !canExecute(file)) return false; + // Don't overwrite existing file just to check if we can write to directory. + testFile = new File(parent, randomUUID().toString()); + } else { + parent.mkdirs(); + if (!parent.isDirectory()) return false; + testFile = file; + } + try { + new FileOutputStream(testFile).close(); + if (!canExecute(testFile)) return false; + return true; + } catch (Throwable ex) { + return false; + } finally { + testFile.delete(); + } + } + + private boolean canExecute (File file) { + try { + Method canExecute = File.class.getMethod("canExecute"); + if ((Boolean)canExecute.invoke(file)) return true; + + Method setExecutable = File.class.getMethod("setExecutable", boolean.class, boolean.class); + setExecutable.invoke(file, true, false); + + return (Boolean)canExecute.invoke(file); + } catch (Exception ignored) { + } + return false; + } + + private File extractFile (String sourcePath, String sourceCrc, File extractedFile) throws IOException { + String extractedCrc = null; + if (extractedFile.exists()) { + try { + extractedCrc = crc(new FileInputStream(extractedFile)); + } catch (FileNotFoundException ignored) { + } + } + + // If file doesn't exist or the CRC doesn't match, extract it to the temp dir. + if (extractedCrc == null || !extractedCrc.equals(sourceCrc)) { + InputStream input = null; + FileOutputStream output = null; + try { + input = readFile(sourcePath); + extractedFile.getParentFile().mkdirs(); + output = new FileOutputStream(extractedFile); + byte[] buffer = new byte[4096]; + while (true) { + int length = input.read(buffer); + if (length == -1) break; + output.write(buffer, 0, length); + } + } catch (IOException ex) { + throw new SharedLibraryLoadRuntimeException("Error extracting file: " + sourcePath + "\nTo: " + extractedFile.getAbsolutePath(), + ex); + } finally { + closeQuietly(input); + closeQuietly(output); + } + } + + return extractedFile; + } + + /** Extracts the source file and calls System.load. Attemps to extract and load from multiple locations. Throws runtime + * exception if all fail. */ + private void loadFile (String sourcePath) { + String sourceCrc = crc(readFile(sourcePath)); + + String fileName = new File(sourcePath).getName(); + + // Temp directory with username in path. + File file = new File(System.getProperty("java.io.tmpdir") + "/libgdx" + System.getProperty("user.name") + "/" + sourceCrc, + fileName); + Throwable ex = loadFile(sourcePath, sourceCrc, file); + if (ex == null) return; + + // System provided temp directory. + try { + file = File.createTempFile(sourceCrc, null); + if (file.delete() && loadFile(sourcePath, sourceCrc, file) == null) return; + } catch (Throwable ignored) { + } + + // User home. + file = new File(System.getProperty("user.home") + "/.libgdx/" + sourceCrc, fileName); + if (loadFile(sourcePath, sourceCrc, file) == null) return; + + // Relative directory. + file = new File(".temp/" + sourceCrc, fileName); + if (loadFile(sourcePath, sourceCrc, file) == null) return; + + // Fallback to java.library.path location, eg for applets. + file = new File(System.getProperty("java.library.path"), sourcePath); + if (file.exists()) { + System.load(file.getAbsolutePath()); + return; + } + + throw new SharedLibraryLoadRuntimeException(ex); + } + + /** @return null if the file was extracted and loaded. */ + private Throwable loadFile (String sourcePath, String sourceCrc, File extractedFile) { + try { + System.load(extractFile(sourcePath, sourceCrc, extractedFile).getAbsolutePath()); + return null; + } catch (Throwable ex) { + return ex; + } + } + + /** Sets the library as loaded, for when application code wants to handle libary loading itself. */ + static public synchronized void setLoaded (String libraryName) { + loadedLibraries.add(libraryName); + } + + static public synchronized boolean isLoaded (String libraryName) { + return loadedLibraries.contains(libraryName); + } + + public static void closeQuietly (Closeable c) { + if (c != null) { + try { + c.close(); + } catch (Throwable ignored) { + } + } + } +} diff --git a/src/main/java/ftbsc/bscv/Boscovicino.java b/src/main/java/ftbsc/bscv/Boscovicino.java index 8d6ab67..cf03753 100644 --- a/src/main/java/ftbsc/bscv/Boscovicino.java +++ b/src/main/java/ftbsc/bscv/Boscovicino.java @@ -6,9 +6,7 @@ import com.mojang.brigadier.tree.CommandNode; import ftbsc.bscv.api.IModule; import ftbsc.bscv.patches.CommandsPatch.CommandsBuiltEvent; -import ftbsc.bscv.system.Friends; -import ftbsc.bscv.system.ModManager; -import ftbsc.bscv.system.Ruler; +import ftbsc.bscv.system.*; import net.minecraft.client.gui.screen.IngameMenuScreen; import net.minecraft.client.gui.widget.button.Button; import net.minecraft.command.CommandSource; @@ -27,6 +25,7 @@ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.IOException; @Mod("bscv") public class Boscovicino implements ICommons { @@ -34,7 +33,7 @@ public class Boscovicino implements ICommons { public static final Logger LOGGER = LogManager.getLogger(); - public static ModManager modManager; //todo this should not be static + public static ModManager modManager; //todo rename private final CommandDispatcher<CommandSource> dispatcher = new CommandDispatcher<>(); @@ -46,7 +45,10 @@ public class Boscovicino implements ICommons { @SuppressWarnings("unused") // it just needs to exist to be used by player private static Ruler ruler; - public Boscovicino() { + public static Bindings bindings; + public static Macros macros; + + public Boscovicino() throws IOException { FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onSetupComplete); ForgeConfigSpec.Builder cfg = new ForgeConfigSpec.Builder(); @@ -59,6 +61,10 @@ public class Boscovicino implements ICommons { Boscovicino.ruler = new Ruler(); + ForgeConfigSpec.Builder bindingSpec = new ForgeConfigSpec.Builder(); + Boscovicino.bindings = new Bindings(bindingSpec); + Boscovicino.macros = new Macros(); + Boscovicino.spec = cfg.build(); ForgeConfigSpec.Builder friendSpec = new ForgeConfigSpec.Builder(); @@ -67,6 +73,7 @@ public class Boscovicino implements ICommons { // register config handler ModLoadingContext.get().registerConfig(Type.CLIENT, spec, "bscv.toml"); ModLoadingContext.get().registerConfig(Type.CLIENT, friendSpec.build(), "friends.toml"); + ModLoadingContext.get().registerConfig(Type.CLIENT, bindingSpec.build(), "bindings.toml"); // Register ourselves for server and other game events we are interested in MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/ftbsc/bscv/system/Bindings.java b/src/main/java/ftbsc/bscv/system/Bindings.java new file mode 100644 index 0000000..7081d42 --- /dev/null +++ b/src/main/java/ftbsc/bscv/system/Bindings.java @@ -0,0 +1,89 @@ +package ftbsc.bscv.system; + +import ftbsc.bscv.Boscovicino; +import ftbsc.bscv.api.IModule; +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.client.util.InputMappings; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.client.registry.ClientRegistry; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +public class Bindings { + public static final int UNBOUND = InputMappings.UNKNOWN.getValue(); + + private final HashMap<KeyBinding, Runnable> executorMap; + + private ForgeConfigSpec.ConfigValue<?> store; + + public Bindings(ForgeConfigSpec.Builder spec) { + MinecraftForge.EVENT_BUS.register(this); + this.executorMap = new HashMap<>(); + this.store = spec + //.comment("actual friend list") + .define("lol", () -> null, (u) -> { + System.out.println(u.getClass().getName()); + return true; + }); + } + + public KeyBinding registerBinding(String name, int key, IModule module) { + KeyBinding kb = this.createBinding(name, key); + this.executorMap.put(kb, module::toggle); + return kb; + } + + public KeyBinding registerBinding(String name, int key, String macroFileName) { + KeyBinding kb = this.createBinding(name, key); + this.executorMap.put(kb, () -> { + Boscovicino.macros.execute(macroFileName); + }); + return kb; + } + + private KeyBinding createBinding(String name, int key) { + KeyBinding kb = new KeyBinding(getBindName(name), UNBOUND, getCategory()); + InputMappings.Input in = 0 <= key && key <= 7 + ? InputMappings.Type.MOUSE.getOrCreate(key) + : InputMappings.Type.KEYSYM.getOrCreate(key); + kb.setKey(in); + ClientRegistry.registerKeyBinding(kb); + return kb; + } + + @SubscribeEvent + public void onKeyPress(InputEvent.KeyInputEvent event) { + this.onKeyPress(event.getKey()); + } + + @SubscribeEvent + public void onKeyPress(InputEvent.MouseInputEvent event) { + this.onKeyPress(event.getButton()); + } + + //TODO on keybind change event + private void onKeyPress(int key) { + long windowId = Minecraft.getInstance().getWindow().getWindow(); + for(KeyBinding kb : this.executorMap.keySet()) { + if(kb.getKey().getValue() == key) { + if(InputMappings.isKeyDown(windowId, key)) this.executorMap.get(kb).run(); + return; + } + } + } + + private static String getBindName(String name) { + return String.format("key.%s.%s", Boscovicino.MOD_ID, name); + } + + private static String getCategory() { + return String.format("key.category.%s", Boscovicino.MOD_ID); + } +} diff --git a/src/main/java/ftbsc/bscv/system/Macros.java b/src/main/java/ftbsc/bscv/system/Macros.java new file mode 100644 index 0000000..ca8d56f --- /dev/null +++ b/src/main/java/ftbsc/bscv/system/Macros.java @@ -0,0 +1,50 @@ +package ftbsc.bscv.system; + +import ftbsc.bscv.Boscovicino; +import party.iroiro.luajava.Lua; +import party.iroiro.luajava.luajit.LuaJit; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; + +public class Macros { + private final Lua lua; + private final HashMap<String, String> macroCache; + + public Macros() throws IOException { + this.lua = new LuaJit(); + this.lua.pushJavaClass(Boscovicino.class); //TODO use instance + this.lua.setGlobal("BSCV"); + this.macroCache = new HashMap<>(); + + // load macros + Path path = Paths.get("macros/"); + if(!Files.isDirectory(path)) { + Files.createDirectories(path); + } + + //TODO lazy load setting + try(DirectoryStream<Path> stream = Files.newDirectoryStream(path, (p) -> p.toFile().isFile())) { + for(Path macro : stream) { + String name = macro.getFileName().toString(); + if(name.endsWith(".lua")) { + String code = String.join("\n", Files.readAllLines(macro)); + this.macroCache.put(name, code); + Boscovicino.bindings.registerBinding(name, Bindings.UNBOUND, name); + } + } + } + } + + public boolean execute(String macroFilePath) { + String code = this.macroCache.get(macroFilePath); + if(code == null) return false; + + this.lua.execute(code); + return true; + } +} diff --git a/src/main/java/ftbsc/bscv/system/ModManager.java b/src/main/java/ftbsc/bscv/system/ModManager.java index 9420d44..9dea139 100644 --- a/src/main/java/ftbsc/bscv/system/ModManager.java +++ b/src/main/java/ftbsc/bscv/system/ModManager.java @@ -39,6 +39,16 @@ public class ModManager { return null; } + @Nullable + public IModule get(String name) { + for (IModule m : this.mods) { + if (m.getName().equals(name)) { + return m; + } + } + return null; + } + public void load() { for (ILoadable module : ServiceLoader.load(ILoadable.class)) { if(module instanceof IModule) { |