Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ maven_group=com.odtheking
archives_base_name=odin-animations
# Mod Dependencies
fabric_kotlin_version=1.13.8+kotlin.2.3.0
fabric_api_version=0.138.3+1.21.10
fabric_api_version=0.138.4+1.21.10
devauth_version=1.2.1
commodore_version=1.0.1
# Other Properties
Expand All @@ -23,4 +23,4 @@ kotlin.code.style=official
minecraft_lwjgl_version=3.3.3

# you can put here either version number (0.0.4) or commit hashes
odin_version=0.1.2
odin_version=0.1.2
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@ private float overrideAttackStrengthScale(float originalValue) {
private void forceInstantItemSwap(ItemStack oldItem, ItemStack newItem, CallbackInfoReturnable<Boolean> cir) {
if (Animations.getShouldNoEquipReset()) cir.setReturnValue(true);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.odtheking.odinaddon.mixin;

import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.odtheking.odinaddon.features.impl.render.Animations;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
Expand All @@ -9,9 +9,9 @@
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {

@ModifyExpressionValue(method = "updateSwingTime", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getCurrentSwingDuration()I"))
private int modifySwingDuration(int original) {
if (Animations.isActive() && Animations.getIgnoreHaste()) return Animations.getSpeed();
@ModifyReturnValue(method = "getCurrentSwingDuration", at = @At("RETURN"))
private int odinaddon$overrideSwingDuration(int original) {
if (Animations.isActive() && Animations.getIgnoreHaste()) return Math.max(Animations.getSpeed(), 1);
return original;
}
}
}
124 changes: 124 additions & 0 deletions src/main/java/com/odtheking/odinaddon/mixin/ModuleConfigMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.odtheking.odinaddon.mixin;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.Gson;
import com.odtheking.odin.clickgui.settings.Saving;
import com.odtheking.odin.clickgui.settings.Setting;
import com.odtheking.odin.config.ModuleConfig;
import com.odtheking.odin.features.Module;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
* Makes ModuleConfig saving "cooperative" when multiple addons accidentally (or intentionally) share the same config file.
*
* Vanilla ModuleConfig#save() overwrites the file with only the modules registered to that ModuleConfig instance.
* If multiple addons create their own ModuleConfig pointing at the same file, they will wipe each other's settings.
*
* This mixin merges the existing on-disk JSON array with the modules being saved, preserving unknown modules.
*/
@Mixin(ModuleConfig.class)
public abstract class ModuleConfigMixin {

@Shadow @Final private HashMap<String, Module> modules;
@Shadow @Final private File file;
@Shadow @Final private static Gson gson;

@Inject(method = "save", at = @At("HEAD"), cancellable = true)
private void odinaddon$mergeSave(CallbackInfo ci) {
// Only affect addon configs (config/odin/addons/*) to avoid changing Odin's own config semantics.
File parent = file.getParentFile();
if (parent == null || !"addons".equalsIgnoreCase(parent.getName())) return;

try {
// Read existing entries (if any) so we can preserve modules we don't own.
LinkedHashMap<String, JsonObject> mergedByName = new LinkedHashMap<>();
ArrayList<String> existingOrder = new ArrayList<>();

try {
String existing = Files.readString(file.toPath());
if (!existing.isBlank()) {
JsonElement parsed = JsonParser.parseString(existing);
if (parsed.isJsonArray()) {
for (JsonElement element : parsed.getAsJsonArray()) {
if (element == null || !element.isJsonObject()) continue;
JsonObject obj = element.getAsJsonObject();
JsonElement nameEl = obj.get("name");
if (nameEl == null || !nameEl.isJsonPrimitive() || !nameEl.getAsJsonPrimitive().isString()) continue;
String key = nameEl.getAsString().toLowerCase(Locale.ROOT);
// Keep first occurrence order; later duplicates will get overwritten by the merge.
if (!mergedByName.containsKey(key)) existingOrder.add(key);
mergedByName.put(key, obj);
}
}
}
} catch (Exception ignored) {
// If reading/parsing fails, fall back to writing only our modules below.
mergedByName.clear();
existingOrder.clear();
}

// Overwrite/append our modules with current in-memory values.
for (Module module : modules.values()) {
JsonObject moduleObj = new JsonObject();
moduleObj.add("name", new JsonPrimitive(module.getName()));
moduleObj.add("enabled", new JsonPrimitive(module.getEnabled()));

JsonObject settingsObj = new JsonObject();
for (Map.Entry<String, Setting<?>> entry : module.getSettings().entrySet()) {
Setting<?> setting = entry.getValue();
if (setting instanceof Saving saving) {
settingsObj.add(entry.getKey(), saving.write());
}
}
moduleObj.add("settings", settingsObj);

String key = module.getName().toLowerCase(Locale.ROOT);
if (!mergedByName.containsKey(key)) existingOrder.add(key);
mergedByName.put(key, moduleObj);
}

// Build the final array, keeping original ordering where possible.
JsonArray out = new JsonArray();
Set<String> written = new HashSet<>();

for (String key : existingOrder) {
JsonObject obj = mergedByName.get(key);
if (obj == null) continue;
out.add(obj);
written.add(key);
}

// Safety: append anything not accounted for in existingOrder.
for (Map.Entry<String, JsonObject> entry : mergedByName.entrySet()) {
if (written.contains(entry.getKey())) continue;
out.add(entry.getValue());
}

Files.writeString(file.toPath(), gson.toJson(out), StandardCharsets.UTF_8);
ci.cancel();
} catch (Exception e) {
// Let the original save() run as a fallback.
}
}
}

14 changes: 13 additions & 1 deletion src/main/kotlin/com/odtheking/odinaddon/OdinAnimations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ import com.odtheking.odin.events.core.EventBus
import com.odtheking.odin.features.ModuleManager
import com.odtheking.odinaddon.features.impl.render.Animations
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents

object OdinAnimations : ClientModInitializer {
// Shared addon config file. Multiple addons can point at the same file safely (see ModuleConfigMixin).
private val config = ModuleConfig("OdinAddon.json")

override fun onInitializeClient() {
listOf(this).forEach { EventBus.subscribe(it) }

ModuleManager.registerModules(ModuleConfig("OdinAddon.json"), Animations)
ModuleManager.registerModules(config, Animations)

ClientPlayConnectionEvents.DISCONNECT.register { _, _ ->
ModuleManager.saveConfigurations()
}

ClientLifecycleEvents.CLIENT_STOPPING.register {
ModuleManager.saveConfigurations()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ object Animations : Module(
@JvmStatic val yaw by NumberSetting("Yaw", 0f, -180, 180, 1, desc = "Rotates your held item. Default: 0")
@JvmStatic val pitch by NumberSetting("Pitch", 0f, -180, 180, 1, desc = "Rotates your held item. Default: 0")
@JvmStatic val roll by NumberSetting("Roll", 0f, -180, 180, 1, desc = "Rotates your held item. Default: 0")
@JvmStatic val ignoreHaste by BooleanSetting("Ignore Effects", false, desc = "Makes the chosen speed override haste modifiers.")
@JvmStatic val speed by NumberSetting("Speed", 6, 0, 32, 1, desc = "Speed of the swing animation.").withDependency { ignoreHaste }
@JvmStatic val ignoreHaste by BooleanSetting("Ignore Effects", false, desc = "Overrides swing-speed effects like Haste/Mining Fatigue.")
@JvmStatic val speed by NumberSetting("Speed", 6, 1, 32, 1, desc = "Speed of the swing animation.").withDependency { ignoreHaste }

val noEquipReset by BooleanSetting("No Equip Reset", false, desc = "Disables the equipping animation when switching items.")
val noSwing by BooleanSetting("No Swing", false, desc = "Prevents your item from visually swinging forward.")
Expand All @@ -30,4 +30,4 @@ object Animations : Module(

@JvmStatic
val isActive get() = enabled
}
}
3 changes: 2 additions & 1 deletion src/main/resources/odinaddon.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"package": "com.odtheking.odinaddon.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"LivingEntityMixin"
"LivingEntityMixin",
"ModuleConfigMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down