initial commit, everything should be working

This commit is contained in:
candle 2025-09-28 00:54:47 -04:00
commit 0b30af9b60
40 changed files with 2134 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
build/
run/
.idea/
.c*
.gradle/
gradle/
.architectury-transformer/

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 candle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

79
build.gradle Normal file
View File

@ -0,0 +1,79 @@
plugins {
id 'dev.architectury.loom' version '1.11-SNAPSHOT' apply false
id 'architectury-plugin' version '3.4-SNAPSHOT'
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
}
architectury {
minecraft = project.minecraft_version
}
allprojects {
group = rootProject.maven_group
version = rootProject.mod_version
}
subprojects {
apply plugin: 'dev.architectury.loom'
apply plugin: 'architectury-plugin'
apply plugin: 'maven-publish'
base {
// Set up a suffixed format for the mod jar names, e.g. `example-fabric`.
archivesName = "$rootProject.archives_name-$project.name"
}
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
maven {
name = "TerraformersMC"
url = "https://maven.terraformersmc.com/"
}
maven {
name = "Shedaniel"
url = "https://maven.shedaniel.me/"
}
}
dependencies {
minecraft "net.minecraft:minecraft:$rootProject.minecraft_version"
mappings loom.officialMojangMappings()
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 17
}
// Configure Maven publishing.
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = base.archivesName.get()
from components.java
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}
}

9
common/build.gradle Normal file
View File

@ -0,0 +1,9 @@
architectury {
common rootProject.enabled_platforms.split(',')
}
dependencies {
modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version"
modImplementation "dev.architectury:architectury:$rootProject.architectury_api_version"
}

View File

@ -0,0 +1,26 @@
package io.lampnet.betterlodestones;
import io.lampnet.betterlodestones.config.ConfigManager;
import io.lampnet.betterlodestones.registry.BetterLodestonesRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class BetterLodestones {
public static final String MOD_ID = "better_lodestones";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static void init() {
BetterLodestonesRegistry.init();
LOGGER.info("Better Lodestones initialized.");
if (ConfigManager.hasLimit()) {
LOGGER.info("Max Lodestones per compass: {}", ConfigManager.getMaxLodestones());
} else {
LOGGER.info("No limit on Lodestones per compass");
}
LOGGER.info("Using {} lodestone recipe",
ConfigManager.useModernLodestoneRecipe() ? "modern (iron)" : "legacy (netherite)");
}
}

View File

@ -0,0 +1,421 @@
package io.lampnet.betterlodestones;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.CompassItem;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public class CompassDataHandler {
private static final String LODESTONES_KEY = "BetterLodestones";
private static final String CURRENT_INDEX_KEY = "CurrentLodestoneIndex";
private static final String POS_X_KEY = "PosX";
private static final String POS_Y_KEY = "PosY";
private static final String POS_Z_KEY = "PosZ";
private static final String DIMENSION_KEY = "Dimension";
private static final String BROKEN_LODESTONE_FLAG = "BrokenLodestone";
public static List<GlobalPos> getLodestones(ItemStack compass) {
List<GlobalPos> lodestones = new ArrayList<>();
CompoundTag tag = compass.getTag();
if (tag == null || !tag.contains(LODESTONES_KEY)) {
return lodestones;
}
ListTag lodestoneList = tag.getList(LODESTONES_KEY, Tag.TAG_COMPOUND);
for (int i = 0; i < lodestoneList.size(); i++) {
CompoundTag lodestoneTag = lodestoneList.getCompound(i);
int x = lodestoneTag.getInt(POS_X_KEY);
int y = lodestoneTag.getInt(POS_Y_KEY);
int z = lodestoneTag.getInt(POS_Z_KEY);
String dimensionString = lodestoneTag.getString(DIMENSION_KEY);
try {
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION,
new ResourceLocation(dimensionString));
BlockPos pos = new BlockPos(x, y, z);
lodestones.add(GlobalPos.of(dimension, pos));
} catch (Exception e) {
// Skip invalid lodestone entries
}
}
return lodestones;
}
public static void addLodestone(ItemStack compass, GlobalPos lodestone) {
CompoundTag tag = compass.getOrCreateTag();
ListTag lodestoneList = tag.getList(LODESTONES_KEY, Tag.TAG_COMPOUND);
for (int i = 0; i < lodestoneList.size(); i++) {
CompoundTag existing = lodestoneList.getCompound(i);
if (isSameLodestone(existing, lodestone)) {
return;
}
}
CompoundTag lodestoneTag = new CompoundTag();
lodestoneTag.putInt(POS_X_KEY, lodestone.pos().getX());
lodestoneTag.putInt(POS_Y_KEY, lodestone.pos().getY());
lodestoneTag.putInt(POS_Z_KEY, lodestone.pos().getZ());
lodestoneTag.putString(DIMENSION_KEY, lodestone.dimension().location().toString());
lodestoneList.add(lodestoneTag);
tag.put(LODESTONES_KEY, lodestoneList);
if (lodestoneList.size() == 1) {
tag.putInt(CURRENT_INDEX_KEY, 0);
}
}
public static void removeLodestone(ItemStack compass, GlobalPos lodestone) {
CompoundTag tag = compass.getTag();
if (tag == null || !tag.contains(LODESTONES_KEY)) {
return;
}
ListTag lodestoneList = tag.getList(LODESTONES_KEY, Tag.TAG_COMPOUND);
int currentIndex = getCurrentLodestoneIndex(compass);
for (int i = lodestoneList.size() - 1; i >= 0; i--) {
CompoundTag existing = lodestoneList.getCompound(i);
if (isSameLodestone(existing, lodestone)) {
lodestoneList.remove(i);
if (i < currentIndex) {
currentIndex--;
} else if (i == currentIndex && currentIndex >= lodestoneList.size()) {
currentIndex = lodestoneList.size() - 1;
}
break;
}
}
if (lodestoneList.isEmpty()) {
tag.remove(CURRENT_INDEX_KEY);
tag.remove(LODESTONES_KEY);
} else {
tag.putInt(CURRENT_INDEX_KEY, Math.max(0, currentIndex));
}
}
public static int getCurrentLodestoneIndex(ItemStack compass) {
CompoundTag tag = compass.getTag();
if (tag == null) {
return 0;
}
return tag.getInt(CURRENT_INDEX_KEY);
}
public static Optional<GlobalPos> getCurrentLodestone(ItemStack compass) {
List<GlobalPos> lodestones = getLodestones(compass);
if (lodestones.isEmpty()) {
return Optional.empty();
}
int index = getCurrentLodestoneIndex(compass);
if (index < 0 || index >= lodestones.size()) {
return Optional.empty();
}
return Optional.of(lodestones.get(index));
}
public static void cycleToNextLodestone(ItemStack compass, Level level) {
List<GlobalPos> lodestones = getLodestones(compass);
if (lodestones.size() <= 1) {
validateCurrentLodestone(compass, level);
return;
}
int currentIndex = getCurrentLodestoneIndex(compass);
int nextIndex = (currentIndex + 1) % lodestones.size();
CompoundTag tag = compass.getOrCreateTag();
tag.putInt(CURRENT_INDEX_KEY, nextIndex);
GlobalPos newLodestone = lodestones.get(nextIndex);
if (isLodestoneValid(level, newLodestone)) {
clearBrokenLodestoneFlag(compass);
} else if (isLodestoneDestroyed(level, newLodestone)) {
markLodestoneAsBroken(compass);
} else {
clearBrokenLodestoneFlag(compass);
}
}
public static int getLodestoneCount(ItemStack compass) {
return getLodestones(compass).size();
}
public static void clearAllLodestones(ItemStack compass) {
CompoundTag tag = compass.getTag();
if (tag != null) {
tag.remove(LODESTONES_KEY);
tag.remove(CURRENT_INDEX_KEY);
tag.remove(BROKEN_LODESTONE_FLAG);
}
}
public static void removeCurrentLodestone(ItemStack compass) {
Optional<GlobalPos> currentLodestone = getCurrentLodestone(compass);
currentLodestone.ifPresent(globalPos -> removeLodestone(compass, globalPos));
}
public static boolean isLodestoneAlreadyBound(ItemStack compass, GlobalPos lodestone) {
CompoundTag tag = compass.getTag();
if (tag == null || !tag.contains(LODESTONES_KEY)) {
return false;
}
ListTag lodestoneList = tag.getList(LODESTONES_KEY, Tag.TAG_COMPOUND);
for (int i = 0; i < lodestoneList.size(); i++) {
CompoundTag existing = lodestoneList.getCompound(i);
if (isSameLodestone(existing, lodestone)) {
return true;
}
}
return false;
}
public static boolean isLodestoneValid(Level level, GlobalPos lodestone) {
if (level == null || lodestone == null) {
return false;
}
if (!level.dimension().equals(lodestone.dimension())) {
return false;
}
return level.getBlockState(lodestone.pos()).is(Blocks.LODESTONE);
}
public static boolean isLodestoneInDifferentDimension(Level level, GlobalPos lodestone) {
if (level == null || lodestone == null) {
return false;
}
return !level.dimension().equals(lodestone.dimension());
}
public static boolean isLodestoneDestroyed(Level level, GlobalPos lodestone) {
if (level == null || lodestone == null) {
return true;
}
if (!level.dimension().equals(lodestone.dimension())) {
return false;
}
return !level.getBlockState(lodestone.pos()).is(Blocks.LODESTONE);
}
public static Optional<GlobalPos> getCurrentValidLodestone(ItemStack compass, Level level) {
Optional<GlobalPos> currentLodestone = getCurrentLodestone(compass);
if (currentLodestone.isEmpty()) {
return Optional.empty();
}
GlobalPos lodestone = currentLodestone.get();
if (isLodestoneValid(level, lodestone)) {
clearBrokenLodestoneFlag(compass);
return currentLodestone;
} else {
markLodestoneAsBroken(compass);
return Optional.empty();
}
}
public static void validateCurrentLodestone(ItemStack compass, Level level) {
Optional<GlobalPos> currentLodestone = getCurrentLodestone(compass);
if (currentLodestone.isEmpty()) {
clearBrokenLodestoneFlag(compass);
return;
}
GlobalPos lodestone = currentLodestone.get();
if (isLodestoneValid(level, lodestone)) {
clearBrokenLodestoneFlag(compass);
} else if (isLodestoneDestroyed(level, lodestone)) {
markLodestoneAsBroken(compass);
} else {
clearBrokenLodestoneFlag(compass);
}
}
public static void markLodestoneAsBroken(ItemStack compass) {
CompoundTag tag = compass.getOrCreateTag();
tag.putBoolean(BROKEN_LODESTONE_FLAG, true);
}
public static boolean hasBrokenLodestone(ItemStack compass) {
CompoundTag tag = compass.getTag();
return tag != null && tag.getBoolean(BROKEN_LODESTONE_FLAG);
}
public static void clearBrokenLodestoneFlag(ItemStack compass) {
CompoundTag tag = compass.getTag();
if (tag != null) {
tag.remove(BROKEN_LODESTONE_FLAG);
}
}
public static void onLodestoneRemoved(Level level, BlockPos removedPos) {
if (level == null || removedPos == null) {
return;
}
GlobalPos removedGlobalPos = GlobalPos.of(level.dimension(), removedPos);
for (Player player : level.players()) {
iteratePlayerCompasses(player, stack -> handleCompassOnRemoval(stack, removedGlobalPos));
}
}
public static void onLodestoneRestored(Level level, BlockPos restoredPos) {
if (level == null || restoredPos == null) {
return;
}
GlobalPos restoredGlobalPos = GlobalPos.of(level.dimension(), restoredPos);
for (Player player : level.players()) {
iteratePlayerCompasses(player, stack -> handleCompassOnRestoration(stack, restoredGlobalPos));
}
}
private static void handleCompassOnRemoval(ItemStack stack, GlobalPos removedGlobalPos) {
Optional<GlobalPos> currentLodestone = getCurrentLodestone(stack);
if (currentLodestone.isEmpty()) {
return;
}
GlobalPos targeted = currentLodestone.get();
if (targeted.dimension().equals(removedGlobalPos.dimension()) && targeted.pos().equals(removedGlobalPos.pos())) {
markLodestoneAsBroken(stack);
}
}
private static void handleCompassOnRestoration(ItemStack stack, GlobalPos restoredGlobalPos) {
if (!hasBrokenLodestone(stack)) {
return;
}
Optional<GlobalPos> currentLodestone = getCurrentLodestone(stack);
if (currentLodestone.isEmpty()) {
return;
}
GlobalPos targeted = currentLodestone.get();
if (targeted.dimension().equals(restoredGlobalPos.dimension()) && targeted.pos().equals(restoredGlobalPos.pos())) {
clearBrokenLodestoneFlag(stack);
}
}
private static boolean isSameLodestone(CompoundTag tag, GlobalPos lodestone) {
int x = tag.getInt(POS_X_KEY);
int y = tag.getInt(POS_Y_KEY);
int z = tag.getInt(POS_Z_KEY);
String dimension = tag.getString(DIMENSION_KEY);
return x == lodestone.pos().getX() &&
y == lodestone.pos().getY() &&
z == lodestone.pos().getZ() &&
dimension.equals(lodestone.dimension().location().toString());
}
public static boolean convertVanillaLodestoneCompass(ItemStack compass) {
if (compass == null || !(compass.getItem() instanceof CompassItem)) {
return false;
}
CompoundTag tag = compass.getTag();
if (tag == null) {
return false;
}
boolean hasVanillaLodestone = tag.contains("LodestonePos") && tag.contains("LodestoneDimension");
if (!hasVanillaLodestone) {
return false;
}
try {
CompoundTag lodestonePos = tag.getCompound("LodestonePos");
String dimensionString = tag.getString("LodestoneDimension");
int x = lodestonePos.getInt("X");
int y = lodestonePos.getInt("Y");
int z = lodestonePos.getInt("Z");
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION,
new ResourceLocation(dimensionString));
BlockPos pos = new BlockPos(x, y, z);
GlobalPos vanillaLodestone = GlobalPos.of(dimension, pos);
addLodestone(compass, vanillaLodestone);
tag.remove("LodestonePos");
tag.remove("LodestoneDimension");
tag.remove("LodestoneTracked");
return true;
} catch (Exception e) {
// If conversion fails, skip this compass
return false;
}
}
public static int convertPlayerLodestoneCompasses(Player player) {
if (player == null) {
return 0;
}
AtomicInteger convertedCount = new AtomicInteger(0);
iteratePlayerCompasses(player, stack -> {
if (convertVanillaLodestoneCompass(stack)) {
convertedCount.incrementAndGet();
}
});
return convertedCount.get();
}
private static void iteratePlayerCompasses(Player player, Consumer<ItemStack> compassConsumer) {
if (player == null) {
return;
}
for (ItemStack stack : player.getInventory().items) {
if (!stack.isEmpty() && stack.getItem() instanceof CompassItem) {
compassConsumer.accept(stack);
}
}
for (ItemStack stack : player.getInventory().armor) {
if (!stack.isEmpty() && stack.getItem() instanceof CompassItem) {
compassConsumer.accept(stack);
}
}
for (ItemStack stack : player.getInventory().offhand) {
if (!stack.isEmpty() && stack.getItem() instanceof CompassItem) {
compassConsumer.accept(stack);
}
}
}
}

View File

@ -0,0 +1,66 @@
package io.lampnet.betterlodestones.advancement;
import com.google.gson.JsonObject;
import io.lampnet.betterlodestones.BetterLodestones;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.advancements.critereon.*;
import net.minecraft.core.GlobalPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public final class DimensionalCompassBindingTrigger extends SimpleCriterionTrigger<DimensionalCompassBindingTrigger.TriggerInstance> {
static final ResourceLocation ID = new ResourceLocation(BetterLodestones.MOD_ID, "dimensional_compass_binding");
@Override
public @NotNull ResourceLocation getId() {
return ID;
}
@Override
public @NotNull TriggerInstance createInstance(JsonObject json, ContextAwarePredicate player, DeserializationContext context) {
int minDimensions = GsonHelper.getAsInt(json, "min_dimensions", 1);
return new TriggerInstance(player, minDimensions);
}
public void trigger(ServerPlayer player, ItemStack compass) {
this.trigger(player, (triggerInstance) -> triggerInstance.matches(compass));
}
public static class TriggerInstance extends AbstractCriterionTriggerInstance {
private final int minDimensions;
public TriggerInstance(ContextAwarePredicate player, int minDimensions) {
super(ID, player);
this.minDimensions = minDimensions;
}
public boolean matches(ItemStack compass) {
List<GlobalPos> lodestones = CompassDataHandler.getLodestones(compass);
if (lodestones.isEmpty()) {
return false;
}
Set<ResourceKey<Level>> uniqueDimensions = lodestones.stream()
.map(GlobalPos::dimension)
.collect(Collectors.toSet());
return uniqueDimensions.size() >= this.minDimensions;
}
@Override
public @NotNull JsonObject serializeToJson(SerializationContext context) {
JsonObject json = super.serializeToJson(context);
json.addProperty("min_dimensions", this.minDimensions);
return json;
}
}
}

View File

@ -0,0 +1,30 @@
package io.lampnet.betterlodestones.config;
public class ConfigManager {
private static int maxLodestones = 0;
private static boolean useModernLodestoneRecipe = true;
public static int getMaxLodestones() {
return maxLodestones;
}
public static void setMaxLodestones(int maxLodestones) {
ConfigManager.maxLodestones = Math.max(0, maxLodestones);
}
public static boolean useModernLodestoneRecipe() {
return useModernLodestoneRecipe;
}
public static void setUseModernLodestoneRecipe(boolean useModernLodestoneRecipe) {
ConfigManager.useModernLodestoneRecipe = useModernLodestoneRecipe;
}
public static boolean hasLimit() {
return getMaxLodestones() > 0;
}
public static boolean isAtLimit(int currentCount) {
return hasLimit() && currentCount >= getMaxLodestones();
}
}

View File

@ -0,0 +1,95 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import io.lampnet.betterlodestones.config.ConfigManager;
import io.lampnet.betterlodestones.registry.BetterLodestonesRegistry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
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(CompassItem.class)
public class CompassItemMixin {
@Inject(method = "useOn", at = @At("HEAD"), cancellable = true)
private void onUseOnBlock(UseOnContext context, CallbackInfoReturnable<InteractionResult> cir) {
Level level = context.getLevel();
BlockPos pos = context.getClickedPos();
ItemStack compass = context.getItemInHand();
Player player = context.getPlayer();
if (!level.isClientSide()) {
CompassDataHandler.convertVanillaLodestoneCompass(compass);
}
if (level.getBlockState(pos).is(Blocks.LODESTONE)) {
if (!level.isClientSide()) {
CompassDataHandler.validateCurrentLodestone(compass, level);
GlobalPos lodestonePos = GlobalPos.of(level.dimension(), pos);
if (CompassDataHandler.isLodestoneAlreadyBound(compass, lodestonePos)) {
CompassDataHandler.removeLodestone(compass, lodestonePos);
int remainingCount = CompassDataHandler.getLodestoneCount(compass);
level.playSound(null, pos, SoundEvents.LODESTONE_COMPASS_LOCK, SoundSource.PLAYERS, 1.0F, 1.0F);
if (player != null) {
player.displayClientMessage(Component.literal(
"§cLodestone unbound: §f" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() +
(remainingCount > 0 ? " §7(" + remainingCount + " remaining)" : "")), true);
}
} else {
int currentCount = CompassDataHandler.getLodestoneCount(compass);
if (ConfigManager.isAtLimit(currentCount)) {
if (player != null) {
int maxLodestones = ConfigManager.getMaxLodestones();
if (maxLodestones == 1) {
player.displayClientMessage(Component.literal(
"§cCompass already bound to a lodestone §7(vanilla mode)"), true);
} else {
player.displayClientMessage(Component.literal(
"§cCompass limit reached: §f" + maxLodestones + " lodestones"), true);
}
}
} else {
CompassDataHandler.addLodestone(compass, lodestonePos);
int lodestoneCount = CompassDataHandler.getLodestoneCount(compass);
level.playSound(null, pos, SoundEvents.LODESTONE_COMPASS_LOCK, SoundSource.PLAYERS, 1.0F, 1.0F);
if (player instanceof ServerPlayer serverPlayer) {
BetterLodestonesRegistry.DIMENSIONAL_COMPASS_BINDING.trigger(serverPlayer, compass);
}
if (player != null) {
String limitInfo = "";
if (ConfigManager.hasLimit()) {
limitInfo = " §7(" + lodestoneCount + "/" + ConfigManager.getMaxLodestones() + ")";
}
player.displayClientMessage(Component.literal(
"§6Lodestone " + lodestoneCount + " bound: §f" +
pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + limitInfo), true);
}
}
}
}
cir.setReturnValue(InteractionResult.sidedSuccess(level.isClientSide()));
}
}
}

View File

@ -0,0 +1,41 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
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;
import java.util.Optional;
@Mixin(CompassItem.class)
public class CompassItemMixinPointing {
@Inject(method = "getLodestonePosition", at = @At("HEAD"), cancellable = true)
private static void getCustomLodestonePosition(CompoundTag tag,
CallbackInfoReturnable<GlobalPos> cir) {
ItemStack tempStack = new ItemStack(Items.COMPASS);
tempStack.setTag(tag);
if (CompassDataHandler.hasBrokenLodestone(tempStack)) {
cir.setReturnValue(null);
return;
}
Optional<GlobalPos> currentLodestone = CompassDataHandler.getCurrentLodestone(tempStack);
currentLodestone.ifPresent(cir::setReturnValue);
}
@Inject(method = "isLodestoneCompass", at = @At("HEAD"), cancellable = true)
private static void isCustomLodestoneCompass(ItemStack stack,
CallbackInfoReturnable<Boolean> cir) {
if (CompassDataHandler.getLodestoneCount(stack) > 0) {
cir.setReturnValue(true);
}
}
}

View File

@ -0,0 +1,117 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import io.lampnet.betterlodestones.config.ConfigManager;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
import java.util.Optional;
@Mixin(Item.class)
public class CompassTooltipMixin {
@Unique
private static final int SELECTED_TICK_INTERVAL = 5;
@Unique
private static final int UNSELECTED_TICK_INTERVAL = 40;
@Inject(method = "appendHoverText", at = @At("TAIL"))
private void addCustomTooltip(ItemStack stack, Level level, List<Component> tooltip, TooltipFlag flag, CallbackInfo ci) {
if (!(stack.getItem() instanceof CompassItem)) {
return;
}
if (level != null && !level.isClientSide()) {
CompassDataHandler.convertVanillaLodestoneCompass(stack);
}
if (level != null) {
CompassDataHandler.validateCurrentLodestone(stack, level);
}
int lodestoneCount = CompassDataHandler.getLodestoneCount(stack);
if (lodestoneCount > 0) {
String countText = "§7Bound Lodestones: §f" + lodestoneCount;
if (ConfigManager.hasLimit()) {
countText += "/" + ConfigManager.getMaxLodestones();
}
tooltip.add(Component.literal(countText));
Optional<GlobalPos> currentLodestone = CompassDataHandler.getCurrentLodestone(stack);
if (currentLodestone.isPresent()) {
GlobalPos lodestone = currentLodestone.get();
BlockPos pos = lodestone.pos();
String dimensionName = lodestone.dimension().location().getPath();
int currentIndex = CompassDataHandler.getCurrentLodestoneIndex(stack) + 1; // 1-based for display
tooltip.add(Component.literal("§7Current: §f" + currentIndex + "/" + lodestoneCount +
" §7at §f" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ()));
if (CompassDataHandler.isLodestoneInDifferentDimension(level, lodestone)) {
tooltip.add(Component.literal("§7Dimension: §c" + better_lodestones$formatDimensionName(dimensionName)));
} else if (CompassDataHandler.hasBrokenLodestone(stack)) {
tooltip.add(Component.literal("§cLodestone destroyed!"));
} else {
tooltip.add(Component.literal("§7Dimension: §f" + better_lodestones$formatDimensionName(dimensionName)));
}
} else if (CompassDataHandler.hasBrokenLodestone(stack)) {
tooltip.add(Component.literal("§cLodestone destroyed!"));
}
}
}
@Inject(method = "inventoryTick", at = @At("HEAD"))
private void onInventoryTick(ItemStack stack, Level level, Entity entity, int slotId, boolean selected, CallbackInfo ci) {
if (!(stack.getItem() instanceof CompassItem)) {
return;
}
if (level != null && !level.isClientSide()) {
int checkInterval = selected ? SELECTED_TICK_INTERVAL : UNSELECTED_TICK_INTERVAL;
if (entity.tickCount % checkInterval == 0) {
CompassDataHandler.convertVanillaLodestoneCompass(stack);
CompassDataHandler.validateCurrentLodestone(stack, level);
}
}
}
@Unique
private String better_lodestones$formatDimensionName(String dimensionName) {
if (dimensionName == null || dimensionName.isEmpty()) {
return "Unknown";
}
String[] words = dimensionName.replace("_", " ").split(" ");
StringBuilder formatted = new StringBuilder();
for (int i = 0; i < words.length; i++) {
if (i > 0) {
formatted.append(" ");
}
String word = words[i];
if (!word.isEmpty()) {
formatted.append(Character.toUpperCase(word.charAt(0)));
if (word.length() > 1) {
formatted.append(word.substring(1).toLowerCase());
}
}
}
return formatted.toString();
}
}

View File

@ -0,0 +1,41 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.ItemStack;
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.CallbackInfo;
@Mixin(AbstractContainerMenu.class)
public class InventoryInteractionMixin {
@Inject(method = "clicked", at = @At("TAIL"))
private void onInventoryClick(int slotId, int button, ClickType clickType, Player player, CallbackInfo ci) {
if (player == null || player.level().isClientSide()) {
return;
}
AbstractContainerMenu menu = (AbstractContainerMenu) (Object) this;
ItemStack carriedItem = menu.getCarried();
if (carriedItem != null && carriedItem.getItem() instanceof CompassItem) {
CompassDataHandler.convertVanillaLodestoneCompass(carriedItem);
}
if (slotId >= 0 && slotId < menu.slots.size()) {
Slot slot = menu.slots.get(slotId);
if (slot != null && slot.hasItem()) {
ItemStack slotItem = slot.getItem();
if (slotItem.getItem() instanceof CompassItem) {
CompassDataHandler.convertVanillaLodestoneCompass(slotItem);
}
}
}
}
}

View File

@ -0,0 +1,32 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.ItemStack;
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.CallbackInfo;
@Mixin(ItemEntity.class)
public class ItemPickupMixin {
@Inject(method = "playerTouch", at = @At("HEAD"))
private void onPlayerTouchItem(Player player, CallbackInfo ci) {
if (player == null || player.level().isClientSide()) {
return;
}
ItemEntity itemEntity = (ItemEntity) (Object) this;
if (itemEntity.isRemoved()) {
return;
}
ItemStack item = itemEntity.getItem();
if (item != null && item.getItem() instanceof CompassItem) {
CompassDataHandler.convertVanillaLodestoneCompass(item);
}
}
}

View File

@ -0,0 +1,75 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
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;
import java.util.Optional;
@Mixin(Item.class)
public class ItemUseMixin {
@Inject(method = "use", at = @At("HEAD"), cancellable = true)
private void onUseInAir(Level level, Player player, InteractionHand hand,
CallbackInfoReturnable<InteractionResultHolder<ItemStack>> cir) {
ItemStack itemStack = player.getItemInHand(hand);
if (!(itemStack.getItem() instanceof CompassItem)) {
return;
}
if (!level.isClientSide()) {
CompassDataHandler.convertVanillaLodestoneCompass(itemStack);
}
if (!level.isClientSide() && player.isShiftKeyDown()) {
CompassDataHandler.validateCurrentLodestone(itemStack, level);
int lodestoneCount = CompassDataHandler.getLodestoneCount(itemStack);
if (lodestoneCount > 1) {
CompassDataHandler.cycleToNextLodestone(itemStack, level);
level.playSound(null, player.blockPosition(), SoundEvents.LODESTONE_COMPASS_LOCK, SoundSource.PLAYERS, 1.0F, 1.2F);
Optional<GlobalPos> currentLodestone = CompassDataHandler.getCurrentLodestone(itemStack);
int currentIndex = CompassDataHandler.getCurrentLodestoneIndex(itemStack) + 1; // 1-based for display
if (currentLodestone.isPresent()) {
BlockPos pos = currentLodestone.get().pos();
player.displayClientMessage(Component.literal(
"§6Lodestone " + currentIndex + "/" + lodestoneCount + ": §f" +
pos.getX() + ", " + pos.getY() + ", " + pos.getZ()), true);
} else if (CompassDataHandler.hasBrokenLodestone(itemStack)) {
player.displayClientMessage(Component.literal("§cAll lodestones destroyed!"), true);
}
cir.setReturnValue(InteractionResultHolder.sidedSuccess(itemStack, level.isClientSide()));
} else if (lodestoneCount == 1) {
if (CompassDataHandler.hasBrokenLodestone(itemStack)) {
player.displayClientMessage(Component.literal("§cLodestone destroyed!"), true);
} else {
player.displayClientMessage(Component.literal("§eOnly one lodestone bound"), true);
}
cir.setReturnValue(InteractionResultHolder.sidedSuccess(itemStack, level.isClientSide()));
} else {
player.displayClientMessage(Component.literal("§cNo lodestones bound"), true);
cir.setReturnValue(InteractionResultHolder.sidedSuccess(itemStack, level.isClientSide()));
}
}
}
}

View File

@ -0,0 +1,35 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
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(Level.class)
public class LevelBlockChangeMixin {
@Inject(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z", at = @At("HEAD"))
private void onBlockChanged(BlockPos pos, BlockState newState, int flags, CallbackInfoReturnable<Boolean> cir) {
Level level = (Level) (Object) this;
if (level == null || level.isClientSide()) {
return;
}
BlockState oldState = level.getBlockState(pos);
if (oldState.is(Blocks.LODESTONE) && !newState.is(Blocks.LODESTONE)) {
CompassDataHandler.onLodestoneRemoved(level, pos);
}
if (!oldState.is(Blocks.LODESTONE) && newState.is(Blocks.LODESTONE)) {
CompassDataHandler.onLodestoneRestored(level, pos);
}
}
}

View File

@ -0,0 +1,25 @@
package io.lampnet.betterlodestones.mixin;
import io.lampnet.betterlodestones.CompassDataHandler;
import io.lampnet.betterlodestones.BetterLodestones;
import net.minecraft.network.Connection;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.players.PlayerList;
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.CallbackInfo;
@Mixin(PlayerList.class)
public class PlayerJoinMixin {
@Inject(method = "placeNewPlayer", at = @At("TAIL"))
private void onPlayerJoin(Connection connection, ServerPlayer player, CallbackInfo ci) {
if (player != null) {
int convertedCount = CompassDataHandler.convertPlayerLodestoneCompasses(player);
if (convertedCount > 0) {
BetterLodestones.LOGGER.info("Converted {} vanilla lodestone compasses for player {}", convertedCount, player.getName().getString());
}
}
}
}

View File

@ -0,0 +1,104 @@
package io.lampnet.betterlodestones.recipe;
import com.google.gson.JsonObject;
import io.lampnet.betterlodestones.CompassDataHandler;
import net.minecraft.core.RegistryAccess;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.CompassItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
public class CompassUnbindingRecipe extends CustomRecipe {
public CompassUnbindingRecipe(ResourceLocation id, CraftingBookCategory category) {
super(id, category);
}
@Override
public boolean matches(CraftingContainer craftingContainer, Level level) {
ItemStack compass = ItemStack.EMPTY;
for (int i = 0; i < craftingContainer.getContainerSize(); ++i) {
ItemStack currentStack = craftingContainer.getItem(i);
if (!currentStack.isEmpty()) {
if (currentStack.getItem() instanceof CompassItem) {
if (!compass.isEmpty()) {
return false; // More than one compass
}
compass = currentStack;
} else {
return false; // Other items in the grid
}
}
}
if (compass.isEmpty() || !compass.hasTag()) {
return false;
}
CompoundTag tag = compass.getTag();
return tag != null && (tag.contains("LodestonePos") || tag.contains("BetterLodestones"));
}
@Override
public @NotNull ItemStack assemble(CraftingContainer craftingContainer, RegistryAccess registryAccess) {
ItemStack compass = ItemStack.EMPTY;
for (int i = 0; i < craftingContainer.getContainerSize(); i++) {
ItemStack currentStack = craftingContainer.getItem(i);
if (!currentStack.isEmpty() && currentStack.getItem() instanceof CompassItem) {
compass = currentStack.copy();
compass.setCount(1);
break;
}
}
if (compass.isEmpty()) {
return ItemStack.EMPTY;
}
CompassDataHandler.convertVanillaLodestoneCompass(compass);
CompassDataHandler.removeCurrentLodestone(compass);
if (CompassDataHandler.getLodestoneCount(compass) == 0) {
return new ItemStack(Items.COMPASS);
}
return compass;
}
@Override
public boolean canCraftInDimensions(int width, int height) {
return width >= 1 && height >= 1;
}
@Override
public @NotNull RecipeSerializer<?> getSerializer() {
return CompassUnbindingRecipeSerializer.INSTANCE;
}
public static class CompassUnbindingRecipeSerializer implements RecipeSerializer<CompassUnbindingRecipe> {
public static final CompassUnbindingRecipeSerializer INSTANCE = new CompassUnbindingRecipeSerializer();
@Override
public @NotNull CompassUnbindingRecipe fromJson(ResourceLocation recipeId, JsonObject json) {
return new CompassUnbindingRecipe(recipeId, CraftingBookCategory.MISC);
}
@Override
public @NotNull CompassUnbindingRecipe fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) {
return new CompassUnbindingRecipe(recipeId, CraftingBookCategory.MISC);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, CompassUnbindingRecipe recipe) {
}
}
}

View File

@ -0,0 +1,77 @@
package io.lampnet.betterlodestones.recipe;
import com.google.gson.JsonObject;
import io.lampnet.betterlodestones.config.ConfigManager;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
public class ConfigurableLodestoneRecipe extends CustomRecipe {
public ConfigurableLodestoneRecipe(ResourceLocation id, CraftingBookCategory category) {
super(id, category);
}
@Override
public boolean matches(CraftingContainer craftingContainer, Level level) {
if (craftingContainer.getWidth() != 3 || craftingContainer.getHeight() != 3) {
return false;
}
Item expectedCenterItem = ConfigManager.useModernLodestoneRecipe() ? Items.IRON_INGOT : Items.NETHERITE_INGOT;
if (!craftingContainer.getItem(4).is(expectedCenterItem)) {
return false;
}
for (int i = 0; i < 9; i++) {
if (i == 4) {
continue;
}
if (!craftingContainer.getItem(i).is(Items.CHISELED_STONE_BRICKS)) {
return false;
}
}
return true;
}
@Override
public @NotNull ItemStack assemble(@NotNull CraftingContainer craftingContainer, RegistryAccess registryAccess) {
return new ItemStack(Items.LODESTONE);
}
@Override
public boolean canCraftInDimensions(int width, int height) {
return width >= 3 && height >= 3;
}
@Override
public @NotNull RecipeSerializer<?> getSerializer() {
return Serializer.INSTANCE;
}
public static class Serializer implements RecipeSerializer<ConfigurableLodestoneRecipe> {
public static final Serializer INSTANCE = new Serializer();
@Override
public @NotNull ConfigurableLodestoneRecipe fromJson(ResourceLocation recipeId, JsonObject json) {
return new ConfigurableLodestoneRecipe(recipeId, CraftingBookCategory.MISC);
}
@Override
public @NotNull ConfigurableLodestoneRecipe fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) {
return new ConfigurableLodestoneRecipe(recipeId, CraftingBookCategory.MISC);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, ConfigurableLodestoneRecipe recipe) {
}
}
}

View File

@ -0,0 +1,29 @@
package io.lampnet.betterlodestones.registry;
import dev.architectury.registry.registries.DeferredRegister;
import dev.architectury.registry.registries.RegistrySupplier;
import io.lampnet.betterlodestones.BetterLodestones;
import io.lampnet.betterlodestones.advancement.DimensionalCompassBindingTrigger;
import io.lampnet.betterlodestones.recipe.CompassUnbindingRecipe;
import io.lampnet.betterlodestones.recipe.ConfigurableLodestoneRecipe;
import net.minecraft.advancements.CriteriaTriggers;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.item.crafting.RecipeSerializer;
public final class BetterLodestonesRegistry {
public static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
DeferredRegister.create(BetterLodestones.MOD_ID, Registries.RECIPE_SERIALIZER);
public static final RegistrySupplier<RecipeSerializer<?>> CONFIGURABLE_LODESTONE =
RECIPE_SERIALIZERS.register("configurable_lodestone", () -> ConfigurableLodestoneRecipe.Serializer.INSTANCE);
public static final RegistrySupplier<RecipeSerializer<?>> COMPASS_UNBINDING =
RECIPE_SERIALIZERS.register("compass_unbinding", () -> CompassUnbindingRecipe.CompassUnbindingRecipeSerializer.INSTANCE);
public static final DimensionalCompassBindingTrigger DIMENSIONAL_COMPASS_BINDING = new DimensionalCompassBindingTrigger();
public static void init() {
RECIPE_SERIALIZERS.register();
CriteriaTriggers.register(DIMENSIONAL_COMPASS_BINDING);
}
}

View File

@ -0,0 +1,4 @@
{
"advancements.better_lodestones.dimensional_lodestone_master.title": "3D Navigator",
"advancements.better_lodestones.dimensional_lodestone_master.description": "Bind a compass to Lodestones in 3 different dimensions"
}

View File

@ -0,0 +1,21 @@
{
"required": true,
"package": "io.lampnet.betterlodestones.mixin",
"compatibilityLevel": "JAVA_17",
"minVersion": "0.8",
"client": [
],
"mixins": [
"CompassItemMixin",
"CompassItemMixinPointing",
"CompassTooltipMixin",
"InventoryInteractionMixin",
"ItemPickupMixin",
"ItemUseMixin",
"LevelBlockChangeMixin",
"PlayerJoinMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@ -0,0 +1,27 @@
{
"parent": "minecraft:nether/use_lodestone",
"display": {
"icon": {
"item": "minecraft:compass",
"nbt": "{CustomModelData:1}"
},
"title": {
"translate": "advancements.better_lodestones.dimensional_lodestone_master.title"
},
"description": {
"translate": "advancements.better_lodestones.dimensional_lodestone_master.description"
},
"frame": "challenge",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"criteria": {
"dimensional_mastery": {
"trigger": "better_lodestones:dimensional_compass_binding",
"conditions": {
"min_dimensions": 3
}
}
}
}

View File

@ -0,0 +1,3 @@
{
"type": "better_lodestones:compass_unbinding"
}

View File

@ -0,0 +1,3 @@
{
"type": "better_lodestones:configurable_lodestone"
}

View File

@ -0,0 +1,41 @@
{
"parent": "minecraft:adventure/root",
"display": {
"icon": {
"item": "minecraft:lodestone"
},
"title": {
"translate": "advancements.nether.use_lodestone.title"
},
"description": {
"translate": "advancements.nether.use_lodestone.description"
},
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"criteria": {
"use_lodestone": {
"trigger": "minecraft:item_used_on_block",
"conditions": {
"location": [
{
"condition": "minecraft:match_tool",
"predicate": {
"items": [
"minecraft:compass"
]
}
}
],
"item": {
"items": [
"minecraft:compass"
]
},
"block": "minecraft:lodestone"
}
}
}
}

View File

@ -0,0 +1,3 @@
{
"type": "better_lodestones:configurable_lodestone"
}

54
fabric/build.gradle Normal file
View File

@ -0,0 +1,54 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
architectury {
platformSetupLoomIde()
fabric()
}
configurations {
common {
canBeResolved = true
canBeConsumed = false
}
compileClasspath.extendsFrom common
runtimeClasspath.extendsFrom common
developmentFabric.extendsFrom common
// Files in this configuration will be bundled into your mod using the Shadow plugin.
// Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files.
shadowBundle {
canBeResolved = true
canBeConsumed = false
}
}
dependencies {
modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version"
modImplementation "net.fabricmc.fabric-api:fabric-api:$rootProject.fabric_api_version"
modImplementation "dev.architectury:architectury-fabric:$rootProject.architectury_api_version"
common(project(path: ':common', configuration: 'namedElements')) { transitive false }
shadowBundle project(path: ':common', configuration: 'transformProductionFabric')
}
processResources {
inputs.property 'version', project.version
filesMatching('fabric.mod.json') {
expand version: project.version
}
}
shadowJar {
configurations = [project.configurations.shadowBundle]
archiveClassifier = 'dev-shadow'
}
remapJar {
input.set shadowJar.archiveFile
}

View File

@ -0,0 +1,13 @@
package io.lampnet.betterlodestones.fabric;
import io.lampnet.betterlodestones.BetterLodestones;
import io.lampnet.betterlodestones.fabric.config.BetterLodestonesFabricConfig;
import net.fabricmc.api.ModInitializer;
public final class BetterLodestonesFabric implements ModInitializer {
@Override
public void onInitialize() {
BetterLodestonesFabricConfig.load();
BetterLodestones.init();
}
}

View File

@ -0,0 +1,64 @@
package io.lampnet.betterlodestones.fabric.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.lampnet.betterlodestones.BetterLodestones;
import io.lampnet.betterlodestones.config.ConfigManager;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class BetterLodestonesFabricConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("better-lodestones.json");
private static ConfigData configData = new ConfigData();
public static class ConfigData {
public int maxLodestones = 0;
public boolean useModernLodestoneRecipe = true;
public ConfigData() {}
}
public static void load() {
if (Files.exists(CONFIG_PATH)) {
try {
String json = Files.readString(CONFIG_PATH);
configData = GSON.fromJson(json, ConfigData.class);
if (configData == null) {
configData = new ConfigData();
}
if (configData.maxLodestones < 0) {
configData.maxLodestones = 0;
}
ConfigManager.setMaxLodestones(configData.maxLodestones);
ConfigManager.setUseModernLodestoneRecipe(configData.useModernLodestoneRecipe);
BetterLodestones.LOGGER.info("Loaded Fabric config: maxLodestones = {}, useModernLodestoneRecipe = {}",
configData.maxLodestones, configData.useModernLodestoneRecipe);
} catch (Exception e) {
BetterLodestones.LOGGER.error("Failed to load Fabric config, using defaults", e);
configData = new ConfigData();
ConfigManager.setMaxLodestones(configData.maxLodestones);
ConfigManager.setUseModernLodestoneRecipe(configData.useModernLodestoneRecipe);
}
} else {
save();
BetterLodestones.LOGGER.info("Created default Fabric config");
}
}
public static void save() {
try {
configData.maxLodestones = ConfigManager.getMaxLodestones();
configData.useModernLodestoneRecipe = ConfigManager.useModernLodestoneRecipe();
Files.createDirectories(CONFIG_PATH.getParent());
String json = GSON.toJson(configData);
Files.writeString(CONFIG_PATH, json);
} catch (IOException e) {
BetterLodestones.LOGGER.error("Failed to save Fabric config", e);
}
}
}

View File

@ -0,0 +1,31 @@
{
"schemaVersion": 1,
"id": "better_lodestones",
"version": "${version}",
"name": "Better Lodestones",
"description": "",
"authors": [
"candle"
],
"contact": {
"homepage": "https://lampnet.io"
},
"license": "MIT",
"icon": "assets/betterlodestones/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"io.lampnet.betterlodestones.fabric.BetterLodestonesFabric"
]
},
"mixins": [
"better_lodestones.mixins.json"
],
"depends": {
"fabricloader": ">=0.17.2",
"minecraft": "~1.20.1",
"java": ">=17",
"architectury": ">=9.2.14",
"fabric-api": "*"
}
}

57
forge/build.gradle Normal file
View File

@ -0,0 +1,57 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
loom {
forge {
mixinConfig "better_lodestones.mixins.json"
}
}
architectury {
platformSetupLoomIde()
forge()
}
configurations {
common {
canBeResolved = true
canBeConsumed = false
}
compileClasspath.extendsFrom common
runtimeClasspath.extendsFrom common
developmentForge.extendsFrom common
// Files in this configuration will be bundled into your mod using the Shadow plugin.
// Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files.
shadowBundle {
canBeResolved = true
canBeConsumed = false
}
}
dependencies {
forge "net.minecraftforge:forge:$rootProject.forge_version"
modImplementation "dev.architectury:architectury-forge:$rootProject.architectury_api_version"
common(project(path: ':common', configuration: 'namedElements')) { transitive false }
shadowBundle project(path: ':common', configuration: 'transformProductionForge')
}
processResources {
inputs.property 'version', project.version
filesMatching('META-INF/mods.toml') {
expand version: project.version
}
}
shadowJar {
configurations = [project.configurations.shadowBundle]
archiveClassifier = 'dev-shadow'
}
remapJar {
input.set shadowJar.archiveFile
}

1
forge/gradle.properties Normal file
View File

@ -0,0 +1 @@
loom.platform=forge

View File

@ -0,0 +1,32 @@
package io.lampnet.betterlodestones.forge;
import io.lampnet.betterlodestones.BetterLodestones;
import io.lampnet.betterlodestones.config.ConfigManager;
import io.lampnet.betterlodestones.forge.config.BetterLodestonesForgeConfig;
import dev.architectury.platform.forge.EventBuses;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.config.ModConfigEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
@Mod(BetterLodestones.MOD_ID)
public final class BetterLodestonesForge {
public BetterLodestonesForge() {
var modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
EventBuses.registerModEventBus(BetterLodestones.MOD_ID, modEventBus);
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, BetterLodestonesForgeConfig.SPEC, "better-lodestones.toml");
modEventBus.addListener(this::onConfigLoad);
BetterLodestones.init();
}
private void onConfigLoad(final ModConfigEvent event) {
if (event.getConfig().getSpec() == BetterLodestonesForgeConfig.SPEC) {
ConfigManager.setMaxLodestones(BetterLodestonesForgeConfig.getMaxLodestones());
ConfigManager.setUseModernLodestoneRecipe(BetterLodestonesForgeConfig.useModernLodestoneRecipe());
BetterLodestones.LOGGER.info("Loaded Forge config and updated ConfigManager.");
}
}
}

View File

@ -0,0 +1,35 @@
package io.lampnet.betterlodestones.forge.config;
import net.minecraftforge.common.ForgeConfigSpec;
public class BetterLodestonesForgeConfig {
public static final ForgeConfigSpec SPEC;
public static final ForgeConfigSpec.IntValue MAX_LODESTONES;
public static final ForgeConfigSpec.BooleanValue USE_MODERN_LODESTONE_RECIPE;
static {
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
builder.comment("Maximum number of Lodestones that can be bound to a single compass")
.comment("0 = No limit (default)")
.comment("1 = Vanilla behavior")
.comment("2-" + Integer.MAX_VALUE + " = Custom limit");
MAX_LODESTONES = builder
.defineInRange("maxLodestones", 0, 0, Integer.MAX_VALUE);
builder.comment("Use modern Lodestone recipe, Iron Ingot instead of Netherite Ingot");
USE_MODERN_LODESTONE_RECIPE = builder
.define("useModernLodestoneRecipe", true);
SPEC = builder.build();
}
public static int getMaxLodestones() {
return MAX_LODESTONES.get();
}
public static boolean useModernLodestoneRecipe() {
return USE_MODERN_LODESTONE_RECIPE.get();
}
}

View File

@ -0,0 +1,35 @@
modLoader = "javafml"
loaderVersion = "[47,)"
#issueTrackerURL = ""
license = "MIT"
[[mods]]
modId = "better_lodestones"
version = "${version}"
displayName = "Better Lodestones"
authors = "candle"
description = '''
'''
#logoFile = ""
[[dependencies.better_lodestones]]
modId = "forge"
mandatory = true
versionRange = "[47,)"
ordering = "NONE"
side = "BOTH"
[[dependencies.better_lodestones]]
modId = "minecraft"
mandatory = true
versionRange = "[1.20.1,)"
ordering = "NONE"
side = "BOTH"
[[dependencies.better_lodestones]]
modId = "architectury"
mandatory = true
versionRange = "[9.2.14,)"
ordering = "AFTER"
side = "BOTH"

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "better_lodestones resources",
"pack_format": 15
}
}

15
gradle.properties Normal file
View File

@ -0,0 +1,15 @@
# Done to increase the memory available to Gradle.
org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true
# Mod properties
mod_version=0.1
maven_group=io.lampnet
archives_name=better-lodestones
enabled_platforms=fabric,forge
# Minecraft properties
minecraft_version=1.20.1
# Dependencies
architectury_api_version=9.2.14
fabric_loader_version=0.17.2
fabric_api_version=0.92.6+1.20.1
forge_version=1.20.1-47.4.9

251
gradlew vendored Executable file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

14
settings.gradle Normal file
View File

@ -0,0 +1,14 @@
pluginManagement {
repositories {
maven { url "https://maven.fabricmc.net/" }
maven { url "https://maven.architectury.dev/" }
maven { url "https://files.minecraftforge.net/maven/" }
gradlePluginPortal()
}
}
rootProject.name = 'better_lodestones'
include 'common'
include 'fabric'
include 'forge'