diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..18bbd9a8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Bug Report +description: "For reporting bugs and other defects" +labels: + - bug +body: + - type: markdown + attributes: + value: >- + **Note: This issue tracker is not intended for support requests!** If you need help with crashes or other issues, then + you should [ask on our Discord server](https://discord.figuramc.org/) instead. +

+ Additionally, please make sure you have done the following: + + - **Have you ensured that all of your mods are up-to-date?** The latest version of Figura + can always be found [on Modrinth](https://modrinth.com/mod/figura) or [on CurseForge](https://curseforge.com/minecraft/mc-mods/figura). + + - **Have you used the [search tool](https://github.com/FiguraMC/Figura/issues) to check whether your issue + has already been reported?** If it has been, then consider adding more information to the existing issue instead. + + - **Have you determined the minimum set of instructions to reproduce the issue?** If your problem only occurs + with other mods installed, then you should narrow down exactly which mods are causing the issue. Please do not + provide your entire list of mods to us and expect that we will be able to figure out the problem. +
+ + This issue template was based [off of Sodium's](https://github.com/CaffeineMC/sodium-fabric/blob/dev/.github/ISSUE_TEMPLATE/bug_report.yml) + - type: textarea + id: description + attributes: + label: Bug Description + description: >- + Use this section to describe the issue you are experiencing in as much depth as possible. The description should + explain what behavior you were expecting, and why you believe the issue to be a bug. If the issue you are reporting + only occurs with specific mods installed, then provide the name and version of each mod. + + **Hint:** If you have any screenshots, videos, or other information that you feel is necessary to + explain the issue, you can attach them here. + - type: textarea + id: description-reproduction-steps + attributes: + label: Reproduction Steps + description: >- + Provide as much information as possible on how to reproduce this bug. Make sure your instructions are as clear and + concise as possible, because other people will need to be able to follow your guide in order to re-create the issue. + + **Hint:** A common way to fill this section out is to write a step-by-step guide. + validations: + required: true + - type: textarea + id: log-file + attributes: + label: Log File + description: >- + **Hint:** You can usually find the log files within the folder `.minecraft/logs`. Most often, you will want the `latest.log` + file, since that file belongs to the last played session of the game. + placeholder: >- + Drag-and-drop the log file here. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..621a3c047 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: true +contact_links: + - name: For help with other issues, join the Discord community + url: https://discord.figuramc.org/ + about: This is the best option for getting help with mod installation, performance issues, and any other support inquiries + # Copied from https://github.com/CaffeineMC/sodium-fabric#community + # Copied from https://github.com/CaffeineMC/sodium-fabric/blob/dev/.github/ISSUE_TEMPLATE/config.yml \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..8728fd571 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: Feature Request +description: "For requesting new features or improvements" +labels: + - enhancement +body: + - type: markdown + attributes: + value: >- + This form is for requesting new features or improvements, and should not be used for bug reports or other issues. + - type: markdown + attributes: + value: >- + Make sure you have used the [search tool](https://github.com/FiguraMC/Figura/issues) to see if a similar + request already exists. If we have previously closed a feature request, then please do not create another request. + - type: markdown + attributes: + value: >- + This template was based [off of Sodium's](https://github.com/CaffeineMC/sodium-fabric/blob/dev/.github/ISSUE_TEMPLATE/feature_request.yml) + - type: textarea + id: description + attributes: + label: Request Description + description: >- + Use this section to describe the feature or improvement that you are looking for. The description should explain + what you would like to see added, and a clear and concise description + of what you would like changed. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 98c5ef503..71c1ce27d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,6 @@ plugins { id "architectury-plugin" version "3.4-SNAPSHOT" - id "dev.architectury.loom" version "1.2-SNAPSHOT" apply false - id "io.github.juuxel.loom-vineflower" version "1.+" apply false + id "dev.architectury.loom" version "1.5-SNAPSHOT" apply false } architectury { @@ -10,7 +9,6 @@ architectury { subprojects { apply plugin: "dev.architectury.loom" - apply plugin: "io.github.juuxel.loom-vineflower" loom { silentMojangMappingsLicense() diff --git a/common/build.gradle b/common/build.gradle index a84ec4ab7..9cd07e5cd 100755 --- a/common/build.gradle +++ b/common/build.gradle @@ -23,6 +23,8 @@ dependencies { implementation("com.github.FiguraMC.luaj:luaj-jse:$luaj-figura") implementation("com.neovisionaries:nv-websocket-client:$nv_websocket") + implementation(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.6")) + // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies // Do NOT use other classes from fabric loader modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" diff --git a/common/src/main/java/org/figuramc/figura/FiguraMod.java b/common/src/main/java/org/figuramc/figura/FiguraMod.java index dcdac5692..1495ec5ca 100644 --- a/common/src/main/java/org/figuramc/figura/FiguraMod.java +++ b/common/src/main/java/org/figuramc/figura/FiguraMod.java @@ -86,8 +86,10 @@ public static void tick() { popPushProfiler("files"); LocalAvatarLoader.tick(); LocalAvatarFetcher.tick(); - popPushProfiler("avatars"); - AvatarManager.tickLoadedAvatars(); + if (Minecraft.getInstance().player != null) { + popPushProfiler("avatars"); + AvatarManager.tickLoadedAvatars(); + } popPushProfiler("chatPrint"); FiguraLuaPrinter.printChatFromQueue(); popPushProfiler("emojiAnim"); diff --git a/common/src/main/java/org/figuramc/figura/animation/Animation.java b/common/src/main/java/org/figuramc/figura/animation/Animation.java index 4de083a70..895571eee 100644 --- a/common/src/main/java/org/figuramc/figura/animation/Animation.java +++ b/common/src/main/java/org/figuramc/figura/animation/Animation.java @@ -101,7 +101,11 @@ public void tick() { else if (inverted && time < offset - loopDelay) time += length + loopDelay - offset; } - case HOLD -> time = inverted ? Math.max(time, offset) : Math.min(time, length); + case HOLD -> { + time = inverted ? Math.max(time, offset) : Math.min(time, length); + if (time == length) + playState = PlayState.HOLDING; + } } this.lastTime = this.frameTime; @@ -158,6 +162,12 @@ public boolean isStopped() { return this.playState == PlayState.STOPPED; } + @LuaWhitelist + @LuaMethodDoc("animation.is_holding") + public boolean isHolding() { + return this.playState == PlayState.HOLDING; + } + @LuaWhitelist @LuaMethodDoc("animation.play") public Animation play() { @@ -598,7 +608,8 @@ public String toString() { public enum PlayState { STOPPED, PAUSED, - PLAYING + PLAYING, + HOLDING } public enum LoopMode { diff --git a/common/src/main/java/org/figuramc/figura/avatar/Avatar.java b/common/src/main/java/org/figuramc/figura/avatar/Avatar.java index 1d6943d5a..a36e61589 100644 --- a/common/src/main/java/org/figuramc/figura/avatar/Avatar.java +++ b/common/src/main/java/org/figuramc/figura/avatar/Avatar.java @@ -341,7 +341,7 @@ public void punish(int amount) { luaRuntime.takeInstructions(amount); } - // -- script events -- // + // -- script events -- // private boolean isCancelled(Varargs args) { if (args == null) @@ -430,6 +430,10 @@ public void resourceReloadEvent() { if (loaded) run("RESOURCE_RELOAD", tick); } + public void damageEvent(String sourceType, EntityAPI sourceCause, EntityAPI sourceDirect, FiguraVec3 sourcePosition) { + if (loaded) run("DAMAGE", tick, sourceType, sourceCause, sourceDirect, sourcePosition); + } + // -- host only events -- // public String chatSendMessageEvent(String message) { // piped event @@ -477,6 +481,10 @@ public void charTypedEvent(String chars, int modifiers, int codePoint) { if (loaded) run("CHAR_TYPED", tick, chars, modifiers, codePoint); } + public boolean totemEvent() { + return isCancelled(loaded ? run("TOTEM",tick) : null); + } + // -- rendering events -- // private void render() { @@ -889,7 +897,7 @@ public void updateMatrices(LivingEntityRenderer entityRenderer, PoseStack } - // -- animations -- // + // -- animations -- // public void applyAnimations() { @@ -919,7 +927,7 @@ public void clearAnimations() { AnimationPlayer.clear(animation); } - // -- functions -- // + // -- functions -- // /** * We should call this whenever an avatar is no longer reachable! @@ -940,8 +948,10 @@ public void clean() { public void clearSounds() { SoundAPI.getSoundEngine().figura$stopSound(owner, null); - for (SoundBuffer value : customSounds.values()) - value.releaseAlBuffer(); + if (SoundAPI.getSoundEngine().figura$isEngineActive()) { + for (SoundBuffer value : customSounds.values()) + value.releaseAlBuffer(); + } } public void closeBuffers() { @@ -978,7 +988,7 @@ private int getVersionStatus() { return version.compareTo(FiguraMod.VERSION); } - // -- loading -- // + // -- loading -- // private void createLuaRuntime() { if (!nbt.contains("scripts")) @@ -1078,9 +1088,13 @@ private void loadCustomSounds() { } public void loadSound(String name, byte[] data) throws Exception { - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data); OggAudioStream oggAudioStream = new OggAudioStream(inputStream)) { - SoundBuffer sound = new SoundBuffer(oggAudioStream.readAll(), oggAudioStream.getFormat()); - this.customSounds.put(name, sound); + if (SoundAPI.getSoundEngine().figura$isEngineActive()) { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data); OggAudioStream oggAudioStream = new OggAudioStream(inputStream)) { + SoundBuffer sound = new SoundBuffer(oggAudioStream.readAll(), oggAudioStream.getFormat()); + this.customSounds.put(name, sound); + } + } else { + FiguraMod.LOGGER.error("Sound is not supported or enabled on this system but a custom sound tried to load anyway, scripts may break."); } } diff --git a/common/src/main/java/org/figuramc/figura/avatar/local/CacheAvatarLoader.java b/common/src/main/java/org/figuramc/figura/avatar/local/CacheAvatarLoader.java index ab41ffe0a..7c8de1eb3 100644 --- a/common/src/main/java/org/figuramc/figura/avatar/local/CacheAvatarLoader.java +++ b/common/src/main/java/org/figuramc/figura/avatar/local/CacheAvatarLoader.java @@ -45,7 +45,7 @@ public static void init() { public static boolean checkAndLoad(String hash, UserData target) { Path p = getAvatarCacheDirectory(); - p = p.resolve(hash + ".moon"); + p = p.resolve(hash + ".nbt"); if (Files.exists(p)) { load(hash, target); @@ -57,7 +57,7 @@ public static boolean checkAndLoad(String hash, UserData target) { public static void load(String hash, UserData target) { LocalAvatarLoader.async(() -> { - Path path = getAvatarCacheDirectory().resolve(hash + ".moon"); + Path path = getAvatarCacheDirectory().resolve(hash + ".nbt"); try { target.loadAvatar(NbtIo.readCompressed(Files.newInputStream(path))); FiguraMod.debug("Loaded avatar \"{}\" from cache to \"{}\"", hash, target.id); @@ -69,7 +69,7 @@ public static void load(String hash, UserData target) { public static void save(String hash, CompoundTag nbt) { LocalAvatarLoader.async(() -> { - Path file = getAvatarCacheDirectory().resolve(hash + ".moon"); + Path file = getAvatarCacheDirectory().resolve(hash + ".nbt"); try { NbtIo.writeCompressed(nbt, Files.newOutputStream(file)); FiguraMod.debug("Saved avatar \"{}\" on cache", hash); diff --git a/common/src/main/java/org/figuramc/figura/avatar/local/LocalAvatarLoader.java b/common/src/main/java/org/figuramc/figura/avatar/local/LocalAvatarLoader.java index b12b46cf5..0acfea8a7 100644 --- a/common/src/main/java/org/figuramc/figura/avatar/local/LocalAvatarLoader.java +++ b/common/src/main/java/org/figuramc/figura/avatar/local/LocalAvatarLoader.java @@ -15,7 +15,10 @@ import org.figuramc.figura.utils.FiguraText; import org.figuramc.figura.utils.IOUtils; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.nio.file.*; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -44,7 +47,7 @@ public class LocalAvatarLoader { CEM_AVATARS.clear(); AvatarManager.clearCEMAvatars(); - for (ResourceLocation resource : manager.listResources("cem", location -> location.endsWith(".moon"))) { + for (ResourceLocation resource : manager.listResources("cem", location -> location.endsWith(".nbt"))) { // id String[] split = resource.getPath().split("/"); if (split.length <= 1) @@ -130,10 +133,15 @@ public static void loadAvatar(Path path, UserData target) { // metadata loadState = LoadState.METADATA; - String metadata = IOUtils.readFile(finalPath.resolve("avatar.json")); - nbt.put("metadata", AvatarMetadataParser.parse(metadata, IOUtils.getFileNameOrEmpty(finalPath))); - AvatarMetadataParser.injectToModels(metadata, models); - AvatarMetadataParser.injectToTextures(metadata, textures); + String _meta = IOUtils.readFile(finalPath.resolve("avatar.json")); + var metadata = AvatarMetadataParser.read(_meta); + + CompoundTag metaNBT = AvatarMetadataParser.parse(metadata,_meta, IOUtils.getFileNameOrEmpty(finalPath)); + nbt.put("metadata", metaNBT); + metaNBT.putString("uuid",target.id.toString()); + + AvatarMetadataParser.injectToModels(metadata, models); + AvatarMetadataParser.injectToTextures(metadata, textures); // return :3 if (!models.isEmpty()) @@ -309,7 +317,7 @@ public static void tick() { Path path = entry.getKey().resolve((Path) event.context()); String name = IOUtils.getFileNameOrEmpty(path); - if (IOUtils.isHidden(path) || !(Files.isDirectory(path) || name.matches("(.*(\\.lua|\\.bbmodel|\\.ogg|\\.png)$|avatar\\.json)"))) + if (IOUtils.isHiddenAvatarResource(path) || !(Files.isDirectory(path) || name.matches("(.*(\\.lua|\\.bbmodel|\\.ogg|\\.png)$|avatar\\.json)"))) continue; if (kind == StandardWatchEventKinds.ENTRY_CREATE && !IS_WINDOWS) diff --git a/common/src/main/java/org/figuramc/figura/commands/ExportCommand.java b/common/src/main/java/org/figuramc/figura/commands/ExportCommand.java index e34b45f36..625849477 100644 --- a/common/src/main/java/org/figuramc/figura/commands/ExportCommand.java +++ b/common/src/main/java/org/figuramc/figura/commands/ExportCommand.java @@ -86,7 +86,7 @@ private static int runAvatarExport(CommandContext con if (avatar.nbt == null) throw new Exception(); - NbtIo.writeCompressed(avatar.nbt, FiguraMod.getFiguraDirectory().resolve(filename + ".moon").toFile()); + NbtIo.writeCompressed(avatar.nbt, FiguraMod.getFiguraDirectory().resolve(filename + ".nbt").toFile()); context.getSource().figura$sendFeedback(new FiguraText("command.export_avatar.success")); return 1; diff --git a/common/src/main/java/org/figuramc/figura/ducks/SoundEngineAccessor.java b/common/src/main/java/org/figuramc/figura/ducks/SoundEngineAccessor.java index d76531a8d..7600b6eba 100644 --- a/common/src/main/java/org/figuramc/figura/ducks/SoundEngineAccessor.java +++ b/common/src/main/java/org/figuramc/figura/ducks/SoundEngineAccessor.java @@ -17,4 +17,5 @@ public interface SoundEngineAccessor { float figura$getVolume(SoundSource category); SoundBufferLibrary figura$getSoundBuffers(); boolean figura$isPlaying(UUID owner); + boolean figura$isEngineActive(); } diff --git a/common/src/main/java/org/figuramc/figura/lua/FiguraLuaJson.java b/common/src/main/java/org/figuramc/figura/lua/FiguraLuaJson.java index f44983dfb..661b6c5a6 100644 --- a/common/src/main/java/org/figuramc/figura/lua/FiguraLuaJson.java +++ b/common/src/main/java/org/figuramc/figura/lua/FiguraLuaJson.java @@ -32,6 +32,7 @@ public String tojstring() { private static final Function TO_JSON_FUNCTION = runtime -> new VarArgFunction() { @Override public Varargs invoke(Varargs args) { + if (args.narg() == 0) return LuaValue.NIL; return LuaValue.valueOf(tableToJsonString(args.arg(1))); } diff --git a/common/src/main/java/org/figuramc/figura/lua/FiguraLuaPrinter.java b/common/src/main/java/org/figuramc/figura/lua/FiguraLuaPrinter.java index 47f3cba2f..853e30644 100644 --- a/common/src/main/java/org/figuramc/figura/lua/FiguraLuaPrinter.java +++ b/common/src/main/java/org/figuramc/figura/lua/FiguraLuaPrinter.java @@ -70,11 +70,6 @@ public static void sendLuaError(LuaError error, Avatar owner) { .replace("\n\t[Java]: in ?", "") .replace("'' expected", "Expected end of script"); - if (Configs.EASTER_EGGS.value && Math.random() < 0.0001) { - message = message - .replaceFirst("attempt to index ? (a nil value) with key", "attempt to key (a ? value) with index nil") - .replaceFirst("attempt to call a nil value", "attempt to nil a call value"); - } // get script line line: { diff --git a/common/src/main/java/org/figuramc/figura/lua/api/AnimationAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/AnimationAPI.java index 9955aa83a..a900b0b82 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/AnimationAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/AnimationAPI.java @@ -4,6 +4,7 @@ import org.figuramc.figura.avatar.Avatar; import org.figuramc.figura.lua.LuaWhitelist; import org.figuramc.figura.lua.docs.LuaMethodDoc; +import org.figuramc.figura.lua.docs.LuaMethodOverload; import org.figuramc.figura.lua.docs.LuaTypeDoc; import java.util.ArrayList; @@ -51,11 +52,20 @@ public List getAnimations() { } @LuaWhitelist - @LuaMethodDoc("animations.get_playing") - public List getPlaying() { + @LuaMethodDoc( + overloads = { + @LuaMethodOverload, + @LuaMethodOverload( + argumentTypes = Boolean.class, + argumentNames = "hold" + ) + }, + value = "animations.get_playing" + ) + public List getPlaying(boolean hold) { List list = new ArrayList<>(); for (Animation animation : avatar.animations.values()) - if (animation.playState == Animation.PlayState.PLAYING) + if (hold ? (animation.playState == Animation.PlayState.PLAYING || animation.playState == Animation.PlayState.HOLDING) : (animation.playState == Animation.PlayState.PLAYING)) list.add(animation); return list; } diff --git a/common/src/main/java/org/figuramc/figura/lua/api/ClientAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/ClientAPI.java index fb28f28cb..03bcd51a6 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/ClientAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/ClientAPI.java @@ -45,6 +45,8 @@ import org.luaj.vm2.LuaValue; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.*; import java.util.function.Supplier; @@ -374,7 +376,7 @@ public static String getShaderPackName() { if (shaderClass == String.class) return (String) shaderNameField.get(null); } - }catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ignored) { + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ignored) { } return ""; } @@ -482,6 +484,11 @@ public static EntityAPI getCameraEntity() { return EntityAPI.wrap(Minecraft.getInstance().getCameraEntity()); } + @LuaWhitelist + @LuaMethodDoc("client.is_integrated_server") + public static Boolean isIntegratedServer() { + return Minecraft.getInstance().getSingleplayerServer() != null; + } @LuaWhitelist @LuaMethodDoc("client.get_server_data") public static Map getServerData() { diff --git a/common/src/main/java/org/figuramc/figura/lua/api/FileAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/FileAPI.java index 3dd64ab34..08dfd2a46 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/FileAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/FileAPI.java @@ -25,8 +25,6 @@ @LuaTypeDoc(name = "FileAPI", value = "file") public class FileAPI { private final Avatar parent; - private static final Path rootFolderPath = FiguraMod.getFiguraDirectory().resolve("data").toAbsolutePath() - .normalize(); private static final String WRITE_NOT_ALLOWED = "You are only allowed to write in the data folder! Anything else is read only!"; public FileAPI(Avatar parent) { @@ -41,10 +39,15 @@ private Path securityCheck(String path) { return p; } + private static Path dataPath() { + return FiguraMod.getFiguraDirectory().resolve("data").toAbsolutePath() + .normalize(); + } + private Path relativizePath(String path) { Path p = Path.of(path); if (p.isAbsolute()) return p.normalize(); - return rootFolderPath.resolve(path).toAbsolutePath().normalize(); + return dataPath().resolve(path).toAbsolutePath().normalize(); } @LuaWhitelist @@ -61,7 +64,7 @@ public boolean isPathAllowed(@LuaNotNil String path) { } public boolean isPathAllowed(Path path) { - return !Files.isSymbolicLink(path) && path.toAbsolutePath().startsWith(rootFolderPath); + return !Files.isSymbolicLink(path) && path.toAbsolutePath().startsWith(dataPath()); } @LuaWhitelist @@ -152,7 +155,7 @@ public FiguraInputStream openReadStream(@LuaNotNil String path) { public FiguraOutputStream openWriteStream(@LuaNotNil String path) { try { Path p = securityCheck(path); - if (!p.startsWith(rootFolderPath)) { + if (!p.startsWith(dataPath())) { throw new LuaError(WRITE_NOT_ALLOWED); } File f = p.toFile(); @@ -173,8 +176,7 @@ public FiguraOutputStream openWriteStream(@LuaNotNil String path) { ) ) public String readString(@LuaNotNil String path, String encoding) { - try (FiguraInputStream fis = openReadStream(path)) { - byte[] data = fis.readAllBytes(); + try { Charset charset = encoding == null ? StandardCharsets.UTF_8 : switch (encoding.toLowerCase(Locale.US)) { case "utf_16", "utf16" -> StandardCharsets.UTF_16; case "utf_16be", "utf16be" -> StandardCharsets.UTF_16BE; @@ -183,6 +185,9 @@ public String readString(@LuaNotNil String path, String encoding) { case "iso_8859_1", "iso88591" -> StandardCharsets.ISO_8859_1; default -> StandardCharsets.UTF_8; }; + Path filePath = securityCheck(path); + File file = filePath.toFile(); + byte[] data = com.google.common.io.Files.toByteArray(file); return new String(data, charset); } catch (IOException e) { throw new LuaError(e); @@ -224,7 +229,7 @@ public void writeString(@LuaNotNil String path, @LuaNotNil String data, String e ) public boolean mkdir(@LuaNotNil String path) { Path p = securityCheck(path); - if (!p.startsWith(rootFolderPath)) { + if (!p.startsWith(dataPath())) { throw new LuaError(WRITE_NOT_ALLOWED); } File f = p.toFile(); @@ -242,7 +247,7 @@ public boolean mkdir(@LuaNotNil String path) { ) public boolean mkdirs(@LuaNotNil String path) { Path p = securityCheck(path); - if (!p.startsWith(rootFolderPath)) { + if (!p.startsWith(dataPath())) { throw new LuaError(WRITE_NOT_ALLOWED); } File f = p.toFile(); @@ -260,7 +265,7 @@ public boolean mkdirs(@LuaNotNil String path) { ) public boolean delete(@LuaNotNil String path) { Path p = securityCheck(path); - if (!p.startsWith(rootFolderPath)) { + if (!p.startsWith(dataPath())) { throw new LuaError(WRITE_NOT_ALLOWED); } File f = p.toFile(); diff --git a/common/src/main/java/org/figuramc/figura/lua/api/HostAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/HostAPI.java index 6aafc4723..b7cff7723 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/HostAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/HostAPI.java @@ -617,6 +617,30 @@ public boolean isChatVerified() { return false; } + @LuaWhitelist + @LuaMethodDoc( + overloads = { + @LuaMethodOverload(argumentTypes = String.class, argumentNames = "string"), + }, + value = "host.write_to_log" + ) + public void writeToLog(@LuaNotNil String string) { + if (!isHost()) return; + FiguraMod.LOGGER.info("[FIGURA/LUA] -- " + string); + } + + @LuaWhitelist + @LuaMethodDoc( + overloads = { + @LuaMethodOverload(argumentTypes = String.class, argumentNames = "string"), + }, + value = "host.warn_to_log" + ) + public void warnToLog(@LuaNotNil String string) { + if (!isHost()) return; + FiguraMod.LOGGER.warn("[FIGURA/LUA] -- " + string); + } + public Object __index(String arg) { if ("unlockCursor".equals(arg)) return unlockCursor; diff --git a/common/src/main/java/org/figuramc/figura/lua/api/TextureAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/TextureAPI.java index cc4fe2504..f650c2460 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/TextureAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/TextureAPI.java @@ -100,7 +100,7 @@ public FiguraTexture read(@LuaNotNil String name, @LuaNotNil Object object) { image = NativeImage.read(null, bais); bais.close(); } catch (Exception e) { - throw new LuaError(e.getMessage()); + throw new LuaError("Could not read image: " + e.getMessage()); } return register(name, image, false); diff --git a/common/src/main/java/org/figuramc/figura/lua/api/entity/EntityAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/entity/EntityAPI.java index a3c1600ca..c02624e8f 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/entity/EntityAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/entity/EntityAPI.java @@ -4,12 +4,11 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.world.Container; import net.minecraft.world.ContainerListener; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityDimensions; -import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.*; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.ProjectileUtil; import net.minecraft.world.item.ItemStack; @@ -35,10 +34,12 @@ import org.figuramc.figura.utils.EntityUtils; import org.figuramc.figura.utils.LuaUtils; import org.jetbrains.annotations.NotNull; +import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -314,6 +315,22 @@ public boolean isCrouching() { return entity.isCrouching(); } + @LuaWhitelist + @LuaMethodDoc("entity.is_moving") + public boolean isMoving() { + checkEntity(); + return entity.getX() != entity.xOld + || entity.getY() != entity.yOld + || entity.getZ() != entity.zOld; + } + + @LuaWhitelist + @LuaMethodDoc("entity.is_falling") + public boolean isFalling() { + checkEntity(); + return !entity.isOnGround() && entity.getY() < entity.yOld; + } + @LuaWhitelist @LuaMethodDoc( overloads = @LuaMethodOverload( @@ -459,6 +476,48 @@ public Object[] getTargetedEntity(Double distance) { return null; } + @LuaWhitelist + @LuaMethodDoc( + overloads = { + @LuaMethodOverload( + argumentTypes = { String.class, Double.class }, + argumentNames = { "type", "radius" } + ), + @LuaMethodOverload( + argumentTypes = { String.class }, + argumentNames = { "type" } + ), + @LuaMethodOverload() + }, + value = "entity.get_nearest_entity" + ) + public EntityAPI getNearestEntity(String type, Double radius) { + checkEntity(); + radius = radius != null ? radius : 20; + + EntityType entityType; + if (type != null) { + ResourceLocation id = ResourceLocation.tryParse(type); + if (id == null) { + throw new LuaError("Invalid entity type: " + type); + } + entityType = Registry.ENTITY_TYPE.get(id); + } else { + entityType = null; + } + + FiguraVec3 pos = getPos(1f); + + AABB aabb = new AABB(pos.offseted(-radius).asVec3(), pos.offseted(radius).asVec3()); + + return getLevel().getEntities(entity, aabb) + .stream() + .filter(e -> entityType == null || e.getType() == entityType) + .min(Comparator.comparingDouble(e -> e.distanceToSqr(pos.x(), pos.y(), pos.z()))) + .map(EntityAPI::wrap) + .orElse(null); + } + @LuaWhitelist @LuaMethodDoc( overloads = { diff --git a/common/src/main/java/org/figuramc/figura/lua/api/event/EventsAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/event/EventsAPI.java index f5594cb3f..a648b5eb8 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/event/EventsAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/event/EventsAPI.java @@ -87,6 +87,11 @@ public class EventsAPI { @LuaWhitelist @LuaFieldDoc("events.resource_reload") public final LuaEvent RESOURCE_RELOAD = new LuaEvent(); + @LuaWhitelist + @LuaFieldDoc("events.totem") + public final LuaEvent TOTEM = new LuaEvent(); + @LuaFieldDoc("events.damage") + public final LuaEvent DAMAGE = new LuaEvent(); private final Map events = new HashMap<>(); @@ -112,6 +117,8 @@ public EventsAPI() { events.put("ITEM_RENDER", ITEM_RENDER); events.put("ON_PLAY_SOUND", ON_PLAY_SOUND); events.put("RESOURCE_RELOAD", RESOURCE_RELOAD); + events.put("TOTEM", TOTEM); + events.put("DAMAGE", DAMAGE); for (FiguraEvent entrypoint : ENTRYPOINTS) { String ID = entrypoint.getID().toUpperCase(Locale.US); diff --git a/common/src/main/java/org/figuramc/figura/lua/api/sound/LuaSound.java b/common/src/main/java/org/figuramc/figura/lua/api/sound/LuaSound.java index 04f16dc52..bd7fbc9e1 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/sound/LuaSound.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/sound/LuaSound.java @@ -9,6 +9,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.TextComponent; import net.minecraft.sounds.SoundSource; + import org.figuramc.figura.avatar.Avatar; import org.figuramc.figura.avatar.AvatarManager; import org.figuramc.figura.lua.LuaWhitelist; @@ -80,7 +81,8 @@ private float calculateVolume() { @LuaWhitelist @LuaMethodDoc("sound.play") public LuaSound play() { - if (this.playing) + // Only skip logic when we truely know the sound is currently playing + if (this.playing && this.handle != null && !this.handle.isStopped()) return this; if (!owner.soundsRemaining.use()) { @@ -90,6 +92,12 @@ public LuaSound play() { owner.noPermissions.remove(Permissions.SOUNDS); + // if we still have a handle reference but the handle has been released, remove the reference + // This occurs when sounds naturally stops + if (this.handle != null && this.handle.isStopped()) { + this.handle = null; + } + // if handle exists, the sound was previously played. Unpause it if (handle != null) { handle.execute(Channel::unpause); @@ -186,8 +194,18 @@ public LuaSound play() { @LuaWhitelist @LuaMethodDoc("sound.is_playing") public boolean isPlaying() { - if (handle != null) - handle.execute(channel -> this.playing = channel.playing()); + if (handle != null) { + // If the handle was released via the sound stopping naturlly, force set to false. + // When a handle is stopped, it has no channel, so playing will not update. + if (handle.isStopped()) + this.playing = false; + // set playing based on whether the sound is paused or not. + else + handle.execute(channel -> this.playing = channel.playing()); + } + // If there is no handle, forcefully set it to false just incase + else + this.playing = false; return this.playing; } diff --git a/common/src/main/java/org/figuramc/figura/lua/api/world/WorldAPI.java b/common/src/main/java/org/figuramc/figura/lua/api/world/WorldAPI.java index b370713f6..f82c4655f 100644 --- a/common/src/main/java/org/figuramc/figura/lua/api/world/WorldAPI.java +++ b/common/src/main/java/org/figuramc/figura/lua/api/world/WorldAPI.java @@ -7,6 +7,7 @@ import net.minecraft.commands.arguments.item.ItemArgument; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; import net.minecraft.core.RegistryAccess; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.ProjectileUtil; @@ -22,6 +23,10 @@ import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.saveddata.maps.MapDecoration; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import net.minecraft.world.phys.AABB; +import org.apache.commons.lang3.ArrayUtils; import org.figuramc.figura.avatar.Avatar; import org.figuramc.figura.avatar.AvatarManager; import org.figuramc.figura.lua.LuaNotNil; @@ -40,6 +45,7 @@ import org.luaj.vm2.LuaTable; import java.util.*; +import java.util.stream.Collectors; @LuaWhitelist @LuaTypeDoc( @@ -164,6 +170,45 @@ public static List getBlocks(Object x, Object y, Double z, Double return list; } + @LuaWhitelist + @LuaMethodDoc( + overloads = { + @LuaMethodOverload( + argumentTypes = String.class, + argumentNames = "id" + ), + }, + value = "world.get_map_data" + ) + public static HashMap getMapData(String id) { + MapItemSavedData data = getCurrentWorld().getMapData(id); + + if (data == null) + return null; + + HashMap map = new HashMap<>(); + + map.put("center_x", data.x); + map.put("center_z", data.z); + map.put("locked", data.locked); + map.put("scale", data.scale); + + ArrayList> decorations = new ArrayList<>(); + for (MapDecoration decoration : data.getDecorations()) { + HashMap decorationMap = new HashMap<>(); + decorationMap.put("type", decoration.getType().toString()); + decorationMap.put("name", decoration.getName() == null ? "" : decoration.getName().getString()); + decorationMap.put("x", decoration.getX()); + decorationMap.put("y", decoration.getY()); + decorationMap.put("rot", decoration.getRot()); + decorationMap.put("image", decoration.getImage()); + decorations.add(decorationMap); + } + map.put("decorations", decorations); + + return map; + } + @LuaWhitelist @LuaMethodDoc( overloads = { @@ -440,6 +485,32 @@ public static Map> getPlayers() { return playerList; } + @LuaWhitelist + @LuaMethodDoc( + overloads = { + @LuaMethodOverload( + argumentTypes = {FiguraVec3.class, FiguraVec3.class}, + argumentNames = {"pos1", "pos2"} + ), + @LuaMethodOverload( + argumentTypes = {Double.class, Double.class, Double.class, Double.class, Double.class, Double.class}, + argumentNames = {"x1", "y1", "z1", "x2", "y2", "z2"} + ) + }, + value = "world.get_entities" + ) + public static List> getEntities(Object x1, Object y1, Double z1, Double x2, Double y2, Double z2) { + Pair pair = LuaUtils.parse2Vec3("getEntities", x1, y1, z1, x2, y2, z2, 1); + FiguraVec3 pos1 = pair.getFirst(); + FiguraVec3 pos2 = pair.getSecond(); + + AABB aabb = new AABB(pos1.asVec3(), pos2.asVec3()); + return getCurrentWorld().getEntitiesOfClass(Entity.class, aabb) + .stream() + .map(EntityAPI::wrap) + .collect(Collectors.toList()); + } + @LuaWhitelist @LuaMethodDoc( overloads = @LuaMethodOverload( diff --git a/common/src/main/java/org/figuramc/figura/mixin/EntityMixin.java b/common/src/main/java/org/figuramc/figura/mixin/EntityMixin.java index ca478642a..c3af3978c 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/EntityMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/EntityMixin.java @@ -1,5 +1,6 @@ package org.figuramc.figura.mixin; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.Vec3; import org.figuramc.figura.avatar.Avatar; @@ -8,23 +9,25 @@ import org.spongepowered.asm.mixin.Intrinsic; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(Entity.class) +@Mixin(value = Entity.class, priority = 999) public class EntityMixin { - @Inject(method = "getEyePosition(F)Lnet/minecraft/world/phys/Vec3;", at = @At("RETURN"), cancellable = true) - private void getEyePosition(float tickDelta, CallbackInfoReturnable cir) { - figura$offsetEyePos(cir); + @ModifyReturnValue(method = "getEyePosition(F)Lnet/minecraft/world/phys/Vec3;", at = @At("RETURN")) + private Vec3 getEyePosition(Vec3 original) { + return figura$offsetEyePos(original); } @Intrinsic - private void figura$offsetEyePos(CallbackInfoReturnable cir) { + private Vec3 figura$offsetEyePos(Vec3 original) { Avatar avatar = AvatarManager.getAvatar((Entity) (Object) this); if (avatar == null || avatar.luaRuntime == null) - return; + return original; + FiguraVec3 vec = avatar.luaRuntime.renderer.eyeOffset; - if (vec != null) cir.setReturnValue(cir.getReturnValue().add(vec.asVec3())); + if (vec != null) return original.add(vec.asVec3()); + + return original; } } diff --git a/common/src/main/java/org/figuramc/figura/mixin/ItemStackMixin.java b/common/src/main/java/org/figuramc/figura/mixin/ItemStackMixin.java index f6b83c3a5..1f9748ded 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/ItemStackMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/ItemStackMixin.java @@ -1,20 +1,20 @@ package org.figuramc.figura.mixin; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import org.figuramc.figura.config.Configs; import org.figuramc.figura.font.Emojis; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(ItemStack.class) +@Mixin(value = ItemStack.class, priority = 999) public class ItemStackMixin { - @Inject(method = "getHoverName", at = @At("RETURN"), cancellable = true) - private void getHoverName(CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "getHoverName", at = @At("RETURN")) + private Component getHoverName(Component original) { if (Configs.EMOJIS.value > 0) - cir.setReturnValue(Emojis.applyEmojis(cir.getReturnValue())); + return Emojis.applyEmojis(original); + return original; } } diff --git a/common/src/main/java/org/figuramc/figura/mixin/LivingEntityMixin.java b/common/src/main/java/org/figuramc/figura/mixin/LivingEntityMixin.java index 9ed911a7c..303cb8156 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/LivingEntityMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/LivingEntityMixin.java @@ -1,5 +1,10 @@ package org.figuramc.figura.mixin; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; @@ -8,7 +13,9 @@ import org.figuramc.figura.avatar.Avatar; import org.figuramc.figura.avatar.AvatarManager; import org.figuramc.figura.ducks.LivingEntityExtension; +import org.figuramc.figura.lua.api.entity.EntityAPI; import org.figuramc.figura.lua.api.world.ItemStackAPI; +import org.figuramc.figura.math.vector.FiguraVec3; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -40,4 +47,17 @@ private void triggerItemUseEffects(ItemStack stack, int particleCount, CallbackI this.animationSpeed += (f - this.animationSpeed) * 0.4f; this.animationPosition += this.animationSpeed; } + + @WrapOperation(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getHurtSound(Lnet/minecraft/world/damagesource/DamageSource;)Lnet/minecraft/sounds/SoundEvent;"), method = "handleEntityEvent") + private SoundEvent handleDamageEvent(LivingEntity instance, DamageSource source, Operation original) { + Avatar avatar = AvatarManager.getAvatar(this); + if (avatar == null) return null; + avatar.damageEvent( + source.msgId, + EntityAPI.wrap(source.getEntity()), + EntityAPI.wrap(source.getDirectEntity()), + source.getSourcePosition() != null ? FiguraVec3.fromVec3(source.getSourcePosition()) : null + ); + return original.call(instance, source); + } } diff --git a/common/src/main/java/org/figuramc/figura/mixin/gui/ChatComponentMixin.java b/common/src/main/java/org/figuramc/figura/mixin/gui/ChatComponentMixin.java index 6908696b1..a3ec8b763 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/gui/ChatComponentMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/gui/ChatComponentMixin.java @@ -29,7 +29,8 @@ import java.util.UUID; import java.util.regex.Pattern; -@Mixin(ChatComponent.class) +// 400 Priority is used as messages must be modified before ChatPatches tries to. +@Mixin(value = ChatComponent.class, priority = 400) public class ChatComponentMixin { @Unique private Integer color; @@ -126,9 +127,9 @@ private Component addMessage(Component message, Component msg, int k, int timest message = TextUtils.replaceInText(message, quotedName, emptyReplacement, (s, style) -> true, isOwner ? 1 : 0, Integer.MAX_VALUE); // sender badges - if (config > 1 && isOwner) { + if (isOwner) { // badges - Component temp = Badges.appendBadges(replacement, uuid, true); + Component temp = Badges.appendBadges(replacement, uuid, config > 1); // trim temp = TextUtils.trim(temp); // modify message, only first diff --git a/common/src/main/java/org/figuramc/figura/mixin/gui/books/BookViewScreenMixin.java b/common/src/main/java/org/figuramc/figura/mixin/gui/books/BookViewScreenMixin.java index c4e42fc74..c8eca009f 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/gui/books/BookViewScreenMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/gui/books/BookViewScreenMixin.java @@ -10,7 +10,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; -@Mixin(BookViewScreen.class) +@Mixin(value = BookViewScreen.class, priority = 1100) public class BookViewScreenMixin { @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Font;draw(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/util/FormattedCharSequence;FFI)I")) public int render(Font font, PoseStack poseStack, FormattedCharSequence formattedCharSequence, float x, float y, int color) { diff --git a/common/src/main/java/org/figuramc/figura/mixin/sound/SoundEngineMixin.java b/common/src/main/java/org/figuramc/figura/mixin/sound/SoundEngineMixin.java index 67fa83c1d..a75e02f2d 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/sound/SoundEngineMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/sound/SoundEngineMixin.java @@ -203,4 +203,9 @@ public void play(SoundInstance sound, CallbackInfo c) { } return false; } + + @Override + public boolean figura$isEngineActive() { + return loaded; + } } diff --git a/common/src/main/java/org/figuramc/figura/parsers/AvatarMetadataParser.java b/common/src/main/java/org/figuramc/figura/parsers/AvatarMetadataParser.java index f57cd8cd1..6b45924b7 100644 --- a/common/src/main/java/org/figuramc/figura/parsers/AvatarMetadataParser.java +++ b/common/src/main/java/org/figuramc/figura/parsers/AvatarMetadataParser.java @@ -33,6 +33,9 @@ public static Metadata read(String json) { public static CompoundTag parse(String json, String filename) { // parse json -> object Metadata metadata = read(json); + return parse(metadata,json,filename); + } + public static CompoundTag parse(Metadata metadata, String json, String filename) { // nbt CompoundTag nbt = new CompoundTag(); @@ -101,11 +104,9 @@ public static CompoundTag parse(String json, String filename) { return nbt; } - - public static void injectToModels(String json, CompoundTag models) throws IOException { + public static void injectToModels(Metadata metadata, CompoundTag models) throws IOException { PARTS_TO_MOVE.clear(); - Metadata metadata = GSON.fromJson(json, Metadata.class); if (metadata != null && metadata.customizations != null) { for (Map.Entry entry : metadata.customizations.entrySet()) injectCustomization(entry.getKey(), entry.getValue(), models); @@ -120,7 +121,21 @@ public static void injectToModels(String json, CompoundTag models) throws IOExce targetPart.put("chld", list); } } + public static void injectToTextures(Metadata metadata, CompoundTag textures) { + if (metadata == null || metadata.ignoredTextures == null) + return; + + CompoundTag src = textures.getCompound("src"); + for (String texture : metadata.ignoredTextures) { + byte[] bytes = src.getByteArray(texture); + int[] size = BlockbenchModelParser.getTextureSize(bytes); + ListTag list = new ListTag(); + list.add(IntTag.valueOf(size[0])); + list.add(IntTag.valueOf(size[1])); + src.put(texture, list); + } + } private static void injectCustomization(String path, Customization customization, CompoundTag models) throws IOException { boolean remove = customization.remove != null && customization.remove; CompoundTag modelPart = getTag(models, path, remove); @@ -198,22 +213,7 @@ private static CompoundTag getTag(CompoundTag models, String path, boolean remov return current; } - public static void injectToTextures(String json, CompoundTag textures) { - Metadata metadata = GSON.fromJson(json, Metadata.class); - if (metadata == null || metadata.ignoredTextures == null) - return; - - CompoundTag src = textures.getCompound("src"); - for (String texture : metadata.ignoredTextures) { - byte[] bytes = src.getByteArray(texture); - int[] size = BlockbenchModelParser.getTextureSize(bytes); - ListTag list = new ListTag(); - list.add(IntTag.valueOf(size[0])); - list.add(IntTag.valueOf(size[1])); - src.put(texture, list); - } - } // json object class public static class Metadata { diff --git a/common/src/main/java/org/figuramc/figura/parsers/BlockbenchModelParser.java b/common/src/main/java/org/figuramc/figura/parsers/BlockbenchModelParser.java index 93917451c..291189621 100644 --- a/common/src/main/java/org/figuramc/figura/parsers/BlockbenchModelParser.java +++ b/common/src/main/java/org/figuramc/figura/parsers/BlockbenchModelParser.java @@ -131,11 +131,27 @@ private void parseTextures(Path avatar, Path sourceFile, String folders, String byte[] source; try { //check the file to load - Path p = sourceFile.resolve(texture.relative_path); + Path p = sourceFile.getParent().resolve(texture.relative_path); if (p.getFileSystem() == FileSystems.getDefault()) { File f = p.toFile().getCanonicalFile(); p = f.toPath(); - if (!f.exists()) throw new IllegalStateException("File do not exists!"); + if (!f.exists()) { + // Compatibility with old Blockbench models. (BB 4.9-) + if (texture.relative_path.startsWith("../")) { + p = sourceFile.resolve(texture.relative_path); + if (p.getFileSystem() == FileSystems.getDefault()) { + f = p.toFile().getCanonicalFile(); + p = f.toPath(); + if (!f.exists()) throw new IllegalStateException("File do not exists!"); + } else { + p = p.normalize(); + if (p.getFileSystem() != avatar.getFileSystem()) + throw new IllegalStateException("File from outside the avatar folder!"); + } + } else { + throw new IllegalStateException("File do not exists!"); + } + } } else { p = p.normalize(); if (p.getFileSystem() != avatar.getFileSystem()) @@ -272,11 +288,17 @@ private CompoundTag parseCubeFaces(JsonObject faces) { continue; //convert face json to java object - BlockbenchModel.CubeFace face = GSON.fromJson(faces.getAsJsonObject(cubeFace), BlockbenchModel.CubeFace.class); - + JsonObject faceObj = faces.getAsJsonObject(cubeFace); //dont add null faces - if (face.texture == null) + if (!faceObj.has("texture")) continue; + try{ + faceObj.get("texture").getAsNumber(); + }catch(Exception e){ + continue; + } + BlockbenchModel.CubeFace face = GSON.fromJson(faceObj, BlockbenchModel.CubeFace.class); + //parse texture TextureData texture = textureMap.get(textureIdMap.get(face.texture)); diff --git a/common/src/main/java/org/figuramc/figura/utils/IOUtils.java b/common/src/main/java/org/figuramc/figura/utils/IOUtils.java index b3fce4dbf..c3c7d4a98 100644 --- a/common/src/main/java/org/figuramc/figura/utils/IOUtils.java +++ b/common/src/main/java/org/figuramc/figura/utils/IOUtils.java @@ -3,6 +3,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; import org.figuramc.figura.FiguraMod; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; @@ -166,6 +167,39 @@ public static boolean isHidden(Path path) { return hidden || getFileNameOrEmpty(path).startsWith("."); } + /** + * Checks, if given file is a hidden avatar resource. + * Avatar resource is hidden, if it is contained within + * a hidden folder. Folder is considered "hidden" if either + * it is marked as "hidden" by the OS or folder's name starts + * with a `{@code .}` (dot), or if it is contained within + * another hidden folder. + * + * @param path Path of the file to check + * @return {@code true} if given file is hidden + */ + public static boolean isHiddenAvatarResource(@NotNull Path path) { + final Path avatarsDirectory = FiguraMod.getFiguraDirectory().resolve("avatars/"); + if (!path.toAbsolutePath().startsWith(avatarsDirectory.toAbsolutePath())) { + throw new IllegalArgumentException("A path to a file within avatars folder was expected"); + } + try { + // Iterate through all parent folders of the avatars + // resource to find a hidden one (if any) + for (Path parent = path; + !Files.isSameFile(parent, avatarsDirectory); + parent = parent.resolve("..").normalize()) { + if (Files.isHidden(parent) || parent.getFileName().toString().startsWith(".")) { + return true; + } + } + return false; + } catch (IOException e) { + FiguraMod.LOGGER.error("Failed to get if \"" + path + "\" is hidden", e); + return false; + } + } + public static class DirWrapper { private final Path path; diff --git a/common/src/main/java/org/figuramc/figura/utils/LuaUtils.java b/common/src/main/java/org/figuramc/figura/utils/LuaUtils.java index b4f82e98b..40e6fdfd7 100644 --- a/common/src/main/java/org/figuramc/figura/utils/LuaUtils.java +++ b/common/src/main/java/org/figuramc/figura/utils/LuaUtils.java @@ -292,7 +292,7 @@ public static JsonElement asJsonValue(LuaValue value) { LuaTable table = value.checktable(); // If it's an "array" (uses numbers as keys) - if (checkTableArray(table)) { + if (checkTableArray(table) && table.length() > 0) { JsonArray arr = new JsonArray(); LuaValue[] keys = table.keys(); int arrayLength = keys[keys.length-1].checkint(); diff --git a/common/src/main/java/org/figuramc/figura/utils/MathUtils.java b/common/src/main/java/org/figuramc/figura/utils/MathUtils.java index cef871e95..eaa6b5df3 100644 --- a/common/src/main/java/org/figuramc/figura/utils/MathUtils.java +++ b/common/src/main/java/org/figuramc/figura/utils/MathUtils.java @@ -171,7 +171,7 @@ public static FiguraVec3 catmullrom(double delta, FiguraVec3 prevA, FiguraVec3 p // no idea how it works // it is the same function from minecraft, but using doubles instead public static double catmullrom(double delta, double prevA, double prevB, double nextA, double nextB) { - return 0.5 * (2 * prevB + (nextA - prevA) * delta + (2 * prevA - 5 * prevB + 4 * nextA - nextB) * delta * delta + (3 * prevB - prevA - 3 * nextA + nextB) * delta * delta * delta); + return 0.5 * (2 * prevB + (-prevA + nextA) * delta + (2 * prevA - 5 * prevB + 4 * nextA - nextB) * delta * delta + (-prevA + 3 * prevB - 3 * nextA + nextB) * delta * delta * delta); } // bezier function generated by ChatGPT @@ -180,26 +180,39 @@ public static double bezier(double t, double p0, double p1, double p2, double p3 return p0 * d * d * d + 3 * p1 * d * d * t + 3 * p2 * d * t * t + p3 * t * t * t; } - // secant method for finding bezier t based on x, also provided by ChatGPT + // newton raphson method for finding bezier t based on x, also provided by ChatGPT + // this approximation method is more accurate and often requires less iterations than secant based public static double bezierFindT(double x, double p0, double p1, double p2, double p3) { - double x0 = 0.4; - double x1 = 0.6; - double tolerance = 0.001; + double tolerance = 0.0001; + double t = 0.5; int iterations = 100; for (int i = 0; i < iterations; i++) { - double fx1 = bezier(x1, p0, p1, p2, p3) - x; - double fx0 = bezier(x0, p0, p1, p2, p3) - x; - double xNext = x1 - fx1 * (x1 - x0) / (fx1 - fx0); - if (Math.abs(xNext - x1) < tolerance) - return xNext; - x0 = x1; - x1 = xNext; + double xBezier = bezier(t, p0, p1, p2, p3); + double dxBezier = bezierDerivative(t, p0, p1, p2, p3); + + if (dxBezier == 0) { + break; // Avoid division by zero + } + + double tNext = t - (xBezier - x) / dxBezier; + + if (Math.abs(tNext - t) < tolerance) { + return tNext; + } + + t = tNext; } - return x1; + return t; } + private static double bezierDerivative(double t, double p0, double p1, double p2, double p3) { + double d = 1 - t; + return 3 * (p1 - p0) * d * d + 6 * (p2 - p1) * d * t + 3 * (p3 - p2) * t * t; + } + + //same as minecraft too, but with doubles and fixing the NaN in the Math.asin public static FiguraVec3 quaternionToYXZ(Quaternion quaternion) { double r, i, j, k; diff --git a/common/src/main/java/org/figuramc/figura/wizards/AvatarWizard.java b/common/src/main/java/org/figuramc/figura/wizards/AvatarWizard.java index 920c6d422..04fd1da40 100644 --- a/common/src/main/java/org/figuramc/figura/wizards/AvatarWizard.java +++ b/common/src/main/java/org/figuramc/figura/wizards/AvatarWizard.java @@ -271,7 +271,7 @@ else if (hasCapeOrElytra) if (hasCape) { Group cape = model.addGroup(Cape, FiguraVec3.of(0, 24, 2), root); Cube cube = model.addCube("Cape", FiguraVec3.of(-5, 8, 2), FiguraVec3.of(10, 16, 1), cape); - cube.generateBoxFaces(0, 0, capeTex, 1, hasPlayer ? 2 : 1); + cube.generateBoxFaces(0, 0, capeTex, 1, 1); } //elytra diff --git a/common/src/main/resources/assets/figura/lang/en_us.json b/common/src/main/resources/assets/figura/lang/en_us.json index 6d4300bc6..7b9f0d88a 100644 --- a/common/src/main/resources/assets/figura/lang/en_us.json +++ b/common/src/main/resources/assets/figura/lang/en_us.json @@ -741,13 +741,14 @@ "figura.docs.wheel_action.set_toggled": "Sets the toggle state of the Action", "figura.docs.animations": "A global API used for control of Blockbench Animations", "figura.docs.animations.get_animations": "Returns a table with all animations", - "figura.docs.animations.get_playing": "Return a table with all playing animations", + "figura.docs.animations.get_playing": "Return a table with all playing animations\nIf true is passed in for hold animations in the HOLDING play state will be included", "figura.docs.animations.stop_all": "Stops all playing (and paused) animations", "figura.docs.animation": "A Blockbench animation", "figura.docs.animation.name": "This animation's name", - "figura.docs.animation.animation.is_playing": "Checks if this animation is being played", - "figura.docs.animation.animation.is_paused": "Checks if this animation is paused", - "figura.docs.animation.animation.is_stopped": "Checks if this animation is stopped", + "figura.docs.animation.is_playing": "Checks if this animation is being played", + "figura.docs.animation.is_paused": "Checks if this animation is paused", + "figura.docs.animation.is_stopped": "Checks if this animation is stopped", + "figura.docs.animation.is_holding": "Checks if this animation is holding on its last frame", "figura.docs.animation.play": "Initializes the animation\nResume the animation if it was paused", "figura.docs.animation.pause": "Pause the animation's playback", "figura.docs.animation.stop": "Stop the animation", @@ -976,6 +977,8 @@ "figura.docs.entity.is_silent": "Returns true if this entity is silent", "figura.docs.entity.is_sneaking": "Returns true if this entity is logically sneaking (can't fall from blocks edges, can't see nameplate behind walls, etc)", "figura.docs.entity.is_crouching": "Returns true if this entity is visually sneaking", + "figura.docs.entity.is_moving": "Returns true if this entity has some velocity", + "figura.docs.entity.is_falling": "Returns true if this entity has negative Y-velocity and is not on the ground", "figura.docs.entity.get_item": "Gets an ItemStack for the item in the given slot\nFor the player, slots are indexed with 1 as the main hand, 2 as the off hand, and 3,4,5,6 as the 4 armor slots from the boots to the helmet\nIf an invalid slot number is given, this will return nil", "figura.docs.entity.get_nbt": "Gets a table containing the NBT of this entity\nPlease note that not all values in the entity's NBT may be synced, as some are handled only on the server side", "figura.docs.entity.is_on_fire": "Returns true if this entity is currently on fire", @@ -988,6 +991,7 @@ "figura.docs.entity.has_inventory": "Checks if the entity has an inventory (Horses, Camels, LLamas, …)", "figura.docs.entity.get_targeted_block": "Returns a proxy for your currently targeted BlockState\nThis BlockState appears on the F3 screen\nThe maximum (and default) distance is 20, minimum is -20\nReturns the block, the hit position, and the targeted block face as three separate values", "figura.docs.entity.get_targeted_entity": "Returns a proxy for your currently targeted Entity\nThis Entity appears on the F3 screen\nMaximum and Default distance is 20, Minimum is 0", + "figura.docs.entity.get_nearest_entity": "Returns the closest entity to this entity\nIf `type` is an entity id, (e.g. `minecraft:bee`), only entities of that type will be considered\nRadius defaults to 20, and controls the size of the area for checking entities as a box expanding in every direction from the player", "figura.docs.entity.get_variable": "Gets the value of a variable this entity stored in themselves using the Avatar API's store() function", "figura.docs.entity.is_living": "Gets if this entity is a Living Entity", "figura.docs.entity.is_player": "Gets if this entity is a Player Entity", @@ -1056,6 +1060,8 @@ "figura.docs.events.item_render": "Called on every one of your items that is being rendered\nIt takes six arguments: the item being rendered, the rendering mode, the position, rotation, and scale that would be applied to the item, and if it's being rendered in the left hand\nReturning a ModelPart parented to Item stops the rendering of this item and will render the returned part instead", "figura.docs.events.on_play_sound": "Called every time a new sound is played\nTakes the following as arguments: the sound's ID, its world position, volume, pitch, if the sound should loop, the sound's category, and the sound's file path\nReturn true to prevent this sound from playing", "figura.docs.events.resource_reload": "Called every time that the client resources are reloaded, allowing you to re-create or update resource texture references", + "figura.docs.events.totem": "Called whenever you use a Totem of Undying to cheat death\nIf returned true the animation is cancelled", + "figura.docs.events.damage": "Called every time you take damage\nTakes four arguments: the damage type as a string, the entity that dealt the damage, the attacking entity, and the damage position\\nThe last three arguments may return nil if there is no direct damage source", "figura.docs.events.get_events": "Returns a table with all events types", "figura.docs.event": "A hook for a certain event in Minecraft\nYou may register functions to one, and those functions will be called when the event occurs", "figura.docs.event.register": "Register a function on this event\nFunctions are run in registration order\nAn optional string argument can be given, grouping functions under that name, for an easier management later on", @@ -1063,6 +1069,8 @@ "figura.docs.event.remove": "Removes either a function from this event or when given a string, remove all functions registered under that name\nReturns the number of functions that were removed", "figura.docs.event.get_registered_count": "Returns the number of functions that are registered with the given name", "figura.docs.host": "A global API dedicated to specifically the host of the avatar\nFor other viewers, these do nothing", + "figura.docs.host.write_to_log": "Write directly to the minecraft log, allowing for\nlogging debug data without filling chat", + "figura.docs.host.warn_to_log": "Write a warning directly to the minecraft log,\nallowing for logging debug data without filling chat", "figura.docs.host.unlock_cursor": "Setting this value to true will unlock your cursor, letting you move it freely on the screen instead of it controlling your player's rotation", "figura.docs.host.is_host": "Returns true if this instance of the script is running on host", "figura.docs.host.is_cursor_unlocked": "Checks if the cursor is currently unlocked\nOnly responds to your own changes in your script, not anything done by Minecraft itself", @@ -1670,6 +1678,7 @@ "figura.docs.world.get_height": "Returns the highest point at the given position according to the provided heightmap\nDefaults to MOTION_BLOCKING if no heightmap is provided", "figura.docs.world.get_dimension": "Gets the dimension name of this world", "figura.docs.world.get_entity": "Returns an EntityAPI object from this UUID's entity, or nil if no entity was found", + "figura.docs.world.get_entities": "Returns a list of entities within the bounding box formed by the two given positions", "figura.docs.world.get_players": "Returns a table containing instances of Player for all players in the world\nThe players are indexed by their names", "figura.docs.world.avatar_vars": "Returns a table containing variables stored from all loaded Avatars \"avatar:store()\" function\nThe table will be indexed by the avatar's owner UUID", "figura.docs.world.new_block": "Parses and creates a new BlockState from the given string\nA world position can be optionally given for the blockstate functions that rely on its position", @@ -1679,6 +1688,7 @@ "figura.docs.world.get_spawn_point": "Returns a vector with the coordinates of the world spawn", "figura.docs.world.raycast_entity": "Raycasts an Entity in the world, returns a map containing the entity and it's position.", "figura.docs.world.raycast_block": "Raycasts a Block in the world, returns a map containing the block and it's position.", + "figura.docs.world.get_map_data": "Takes a string, e.g., `map_3`, and returns a table of data if the map exists.\nMap data may be unsynced, and will only update when holding the map", "figura.docs.data": "A global API that provides functions to work with data related features", "figura.docs.data.create_buffer": "Creates an empty buffer", "figura.docs.buffer": "A byte buffer object", diff --git a/common/src/main/resources/assets/figura/textures/gui/help_icons.png b/common/src/main/resources/assets/figura/textures/gui/help_icons.png index 73dd3cdcb..2f91032c3 100644 Binary files a/common/src/main/resources/assets/figura/textures/gui/help_icons.png and b/common/src/main/resources/assets/figura/textures/gui/help_icons.png differ diff --git a/fabric/build.gradle b/fabric/build.gradle index 359fc6138..c86fb81eb 100755 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -30,6 +30,7 @@ dependencies { include(implementation("com.github.FiguraMC.luaj:luaj-core:$luaj-figura")) include(implementation("com.github.FiguraMC.luaj:luaj-jse:$luaj-figura")) include(implementation("com.neovisionaries:nv-websocket-client:$nv_websocket")) + include(implementation(annotationProcessor("io.github.llamalad7:mixinextras-fabric:0.3.6"))) if(rootProject.run_on_quilt == "false") { modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index ad3e544a0..046731229 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -35,7 +35,7 @@ "depends": { "java": ">=${java_version}", "minecraft": "${minecraft_version}", - "fabricloader": ">=0.14.21" + "fabricloader": ">=0.14.25" }, "conflicts": { "immersive_portals": "*", diff --git a/forge/build.gradle b/forge/build.gradle index dd3dbe6b7..ea9674a94 100755 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -34,6 +34,9 @@ dependencies { officialMojangMappings() } + compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.6")) + implementation(include("io.github.llamalad7:mixinextras-forge:0.3.6")) + // Libraries include(forgeRuntimeLibrary("com.github.FiguraMC.luaj:luaj-core:$luaj-figura")) include(forgeRuntimeLibrary("com.github.FiguraMC.luaj:luaj-jse:$luaj-figura")) diff --git a/gradle.properties b/gradle.properties index 3d2c52d2f..e1d0f9bf6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ mappings = 26 enabled_platforms = fabric,forge # Mod Properties -mod_version = 0.1.4 +mod_version = 0.1.5-rc.1 maven_group = org.figuramc archives_base_name = figura assets_version = v2 @@ -24,7 +24,7 @@ nv_websocket = 2.14 # https://fabricmc.net/develop # https://modrinth.com/mod/fabric-api fabric_api = 0.77.0+1.18.2 -fabric_loader_version = 0.14.22 +fabric_loader_version = 0.14.25 # Mod Dependencies # https://modrinth.com/mod/modmenu @@ -52,7 +52,7 @@ quilt_fabric_api_version = 1.0.0-beta.28+0.67.0-1.18.2 # Extra properties run_on_quilt = false run_with_modmenu = false -run_with_geckolib = true +run_with_geckolib = false jarVersion # To run on quilt you must set this to true and go to fabric/gradle.properties to uncomment "loom.platform = quilt",